Vue3 uses canvas to implement layer movement, scaling, rotation and other editing operations

1. Canvas click area detection and how to monitor the click events of various graphics on Canvas

1. Harness the power of mathematics

Some simple and regular graphics, such as rectangles, ellipses, polygons... We can use some mathematical functions to calculate the judgment.

This is great and can be very fast when you don't have a lot of graphics on your canvas.

But it is difficult to handle those very complex geometries in this way. Say, for example, that you are working with lines that have conic curves.

2. Simulate the click area

The idea of ​​the click area is very simple, we only need to get the pixels of the click area and find the graphics with the same color.

But this method may not work, because different graphics may have the same color. To avoid this problem, we should create a hidden " click canvas " that will have almost the same shapes as the main canvas , and each shape will have a unique color. So we need to generate random colors for each circle.

Then, we need to draw each graph 2 times. First time on the main canvas (visible), then on the " click canvas " (invisible).

When you click on the main canvas, all you need to do is to get the coordinates of the position you clicked on, and then find the color of the pixel at the same position as the main cavnas on the " click canvas ".

The main bottleneck of this approach is that you need to draw twice. So the performance may drop by 2 times!

But we can simplify the drawing of hitCanvas, for example, skip the drawing of shadows or strokes, and simplify the graphics, for example, use rectangles instead of text. Simplified way after drawing can be very fast. Because it is a very fast operation to add a pixel from the canvas and get a value from a color hash object (colorsHash).

// 主canvas
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
 
// 点击canvas
const hitCanvas = document.createElement('canvas');
const hitCtx = hitCanvas.getContext('2d');
 
// 颜色对象
const colorsHash = {};
 
// 生成随机颜色
function getRandomColor() {
 const r = Math.round(Math.random() * 255);
 const g = Math.round(Math.random() * 255);
 const b = Math.round(Math.random() * 255);
 return `rgb(${r},${g},${b})`;
}
 
 
// 要绘制的图形对象数组
const circles = [{
  id: '1', x: 40, y: 40, radius: 10, color: 'rgb(255,0,0)'
}, {
  id: '2', x: 100, y: 70, radius: 10, color: 'rgb(0,255,0)'
}];
 
circles.forEach(circle => {
  while(true) {
     const colorKey = getRandomColor();

     if (!colorsHash[colorKey]) {
        circle.colorKey = colorKey;

        colorsHash[colorKey] = circle;

        return;
     }
  }
});
 
// 绘制图形
circles.forEach(circle => {
  // 主canvas
  ctx.beginPath();
  ctx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI, false);
  ctx.fillStyle = circle.color;
  ctx.fill();
  
  // 点击canvas
  hitCtx.beginPath();
  hitCtx.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI, false);
  hitCtx.fillStyle = circle.colorKey;
  hitCtx.fill();
});
 
 
// 监听点击事件
canvas.addEventListener('click', (e) => {
  // 获取鼠标点击位置坐标
  const mousePos = {
    x: e.clientX - canvas.offsetLeft,
    y: e.clientY - canvas.offsetTop
  };

  // 获取点击位置的像素
  const pixel = hitCtx.getImageData(mousePos.x, mousePos.y, 1, 1).data;
  const color = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`;
  const shape = colorsHash[color];

  // 判断是否颜色对象为空
  if (shape) {
     alert('click on circle: ' + shape.id);
  }
});

2. Sorting JSON objects

As we all know, json objects are not ordered. Only arrays have sorting functionality.

But in the business scenarios we encountered, not only arrays need to be sorted, but objects also need to be sorted.

let data = {
    a: {age: 18, height: 189}, 
    b: {age: 18, height: 175}
}
let map = new Map()

for (let k in data) {
    map.set(k, data[k])
}

let arrayObj = Array.from(map)

// 根据 age 排序
arrayObj.sort((a, b) => {
    return b[1]['age'] -a[1]['age']
})

At this point, you will get a new array. You print it out and find that the format has changed, but what we want is still the json format at the beginning, so just convert it back.

let obj = {}

for (let i in arrayObj) {
   let k = arrayObj[i][0]
   let value = arrayObj[i][1]
   obj[k] = value
}

If you want to convert it into a map, you can do it as follows:

var result = new Map(arrayObj.map(i => [i[0], i[1]]));

 

 3. Artboard resolution conversion

// 默认分辨率
let defaultRealResolutionPower = '720*1280'

// 转化分辨率
function formResolutionPower(resolutionPower: String = defaultRealResolutionPower) {

      // 分辨率
      if (resolutionPower) {
        let _sarr = resolutionPower.split("*");

        let _arr = [Number(_sarr[0]), Number(_sarr[1])];

        // 设置在画板里绘制的实际分辨率
        this.realResolutionPower = _arr

        // 获取包裹canvas的外层标签的属性
        const winBox = document.getElementById('winBox')?.getBoundingClientRect()

        // 判断横竖屏
        if (_arr[0] > _arr[1]) {
          this.sDpi = _arr[0] / winBox?.width
          // 编辑分辨率
          this.resolutionPower = JSON.parse(JSON.stringify([715, _arr[1] / this.sDpi]))
        } else {
          this.sDpi = _arr[1] / winBox?.height
          // 编辑分辨率
          this.resolutionPower = JSON.parse(JSON.stringify([_arr[0] / this.sDpi, 512]))
        }

        this.$nextTick(() => {
          const canvasBox = document.getElementById('canvasBox')?.getBoundingClientRect()

          // 设置模版可编辑区域的宽高和左上角坐标
          this.toucheWrap = {
            width: this.resolutionPower[0],
            height: this.resolutionPower[1],
            top: canvasBox.top,
            left: canvasBox.left
          }

          console.log(this.toucheWrap)

        })

        this.callBack('formResolutionPower')

      }
}

4. Draw the main canvas and click on the canvas

        <canvas 
          id="canvas" 
          ref="canvas" 
          style="display: block; width: 100%; height: 100%;"
          @mousedown.left.stop.prevent="wrapStart($event)" 
          @mousemove.left.stop.prevent="wrapMove($event)"
          @mouseup.stop.prevent="wrapUp($event)" 
          @mouseleave.stop.prevent="wrapUp($event)"
          @mousedown.right.stop.prevent="wrapStart($event, true)"></canvas>

 

    // 初始化画板
    initCanvas() {
      console.log('初始化画板', this.realResolutionPower)

      // 真实画板
      if(!this.canvas) {
        this.canvas = document.getElementById('canvas')
        this.ctx = this.canvas.getContext('2d')
      }
      this.canvas.width = this.realResolutionPower[0]
      this.canvas.height = this.realResolutionPower[1]

      // 模拟画板
      if(!this._canvas) {
        this._canvas = document.createElement('canvas')
        this._ctx = this._canvas.getContext('2d')
      }
      this._canvas.width = this.realResolutionPower[0]
      this._canvas.height = this.realResolutionPower[1]

      // 初始化video
      if(!this._video) {
        this._video = document.createElement('video')
        this._video.setAttribute('autoplay', true)
        this._video.setAttribute('loop', true)
      }

    },

    // 生成随机颜色
    getRandomColor() {
      const r = Math.round(Math.random() * 255)
      const g = Math.round(Math.random() * 255)
      const b = Math.round(Math.random() * 255)
      // const a = Math.round(Math.random() * 255)
      return `rgb(${r},${g},${b})`
    },

    // 绘制视频片段
    drawSpPartItem() {
      const _items = JSON.parse(JSON.stringify(this.items))

      _items.sort((a, b) => a.zIndex - b.zIndex)
      // console.log('绘制图层:', _items)

      this.ctx.clearRect(0, 0, this.realResolutionPower[0], this.realResolutionPower[1])
      this._ctx.clearRect(0, 0, this.realResolutionPower[0], this.realResolutionPower[1])

      this._tcList = [] // 模拟点击的图层列表

      let activeItem = null;

      for (let i = 0; i < _items.length; i++) {
        let item = _items[i]

        if (!item.display || !item.show) {
          continue
        }

        if (['bg', 'tt', 'ai'].includes(item.type)) {

          // 视频
          if (item.iobsType == 'video') {
            if (this._video.src !== item.content) {
              this._video.src = item.content
              console.log(this._video)
            }

            this.ctx.drawImage(this._video, item.left, item.top, item.width, item.height)
          } else {
            // 图片
            const img = new Image()
            // img.crossOrigin = 'anonymous'
            img.src = item.content
            // const img = await this.loadImage(item.content)

            this.ctx.drawImage(img, item.left, item.top, item.width, item.height)
          
          }

          // 锁定
          if(!item.lock) {
            // 绘制模拟canvas
            const _color = this.getRandomColor()
            this._tcList.push({
              id: item.id,
              color: _color
            })
            this._ctx.save()
            this._ctx.fillStyle = _color
            this._ctx.fillRect(item.left, item.top, item.width, item.height)
            this._ctx.restore()
          }
          

        } else if (['zm', 'bt'].includes(item.type)) {

          this.ctx.save()
          this.ctx.font = item.fontSize + 'px ' + item.fontFamily
          this.ctx.textAlign = item.align

          let showStroke = false
          if (item.strokeShow && item.strokeSize) {
            this.ctx.strokeStyle = item.strokeColor
            this.ctx.lineWidth = item.strokeSize
            showStroke = true
          }

          this.ctx.textBaseline = 'top'

          this.ctx.fillStyle = item.fontBgColor

          // const measureText = this.ctx.newMeasureText(item.content)

          // 绘制文字背景色
          this.ctx.fillRect(item.left, item.top, item.width, item.height)

          // 绘制文字
          this.ctx.fillStyle = item.fontColor

          const _content = item.type == 'zm' ? '智能字幕' : item.content

          // this.ctx.strokeText(item.content, item.left, item.top)
          if (item.align == 'center') {
            this.ctx.wrapText(showStroke, _content, item.left + item.width / 2, item.top, item.width, item.fontSize)
          } 
          else if (item.align == 'right') {
            this.ctx.wrapText(showStroke, _content, item.left + item.width, item.top, item.width, item.fontSize)
          } 
          else {
            this.ctx.wrapText(showStroke, _content, item.left, item.top, item.width, item.fontSize)
          }
          this.ctx.restore()

          // 锁定
          if(!item.lock) {
            // 绘制模拟canvas
            const _color = this.getRandomColor()
            this._tcList.push({
              id: item.id,
              color: _color
            })
            this._ctx.save()
            this._ctx.fillStyle = _color
            this._ctx.fillRect(item.left, item.top, item.width, item.height)
            this._ctx.restore()
          }
        }

        // 绘制编辑框
        if (item.active && !item.lock) {
          activeItem = item
        }

      }

      if (activeItem) {
        this.drawEditBox(activeItem)
      }
    },

    // 绘制图层
    async drawItems() {
      cancelAnimationFrame(this.requestAnimationFrameId)

      if(this.previewStatus === -1) {
        this.drawSpPartItem()
      } else {
        this.drawPreview()
      }

      this.requestAnimationFrameId = requestAnimationFrame(this.drawItems)
    },

    // 绘制编辑边框
    drawEditBox(item) {

      this.ctx.save()
      const r = 10 // 圆半径
      const w = 20 // 矩形宽度

      // 绘制矩形
      this.ctx.lineWidth = 5
      this.ctx.strokeStyle = '#779BF1'
      this.ctx.strokeRect(item.left, item.top, item.width, item.height)

      // 阴影
      this.ctx.shadowBlur = 5;
      this.ctx.shadowColor = "#779BF1";

      let _color
      /**
       * 
       * 拉伸按钮
       * 
       */

      if(item.type === 'zm') {

        // 中上
        this.ctx.fillStyle = 'white'
        this.ctx.fillRect(item.left + item.width/2 - w/2, item.top - w/2, w, w)

        // 绘制模拟canvas
        _color = this.getRandomColor()
        this._tcList.push({
          type: 'lh-btn',
          cursor: 'ns-resize', // 上下
          id: item.id,
          color: _color
        })
        this._ctx.fillStyle = _color
        this._ctx.fillRect(item.left + item.width/2 - w/2, item.top - w/2, w, w)


        // 中下
        this.ctx.fillStyle = 'white'
        this.ctx.fillRect(item.left + item.width/2 - w/2, item.top + item.height - w/2, w, w)

        // 绘制模拟canvas
        _color = this.getRandomColor()
        this._tcList.push({
          type: 'lh-btn',
          cursor: 'ns-resize', // 上下
          id: item.id,
          color: _color
        })
        this._ctx.fillStyle = _color
        this._ctx.fillRect(item.left + item.width/2 - w/2, item.top + item.height - w/2, w, w)


        // 中左
        this.ctx.fillStyle = 'white'
        this.ctx.fillRect(item.left - w/2, item.top + item.height/2 - w/2, w, w)

        // 绘制模拟canvas
        _color = this.getRandomColor()
        this._tcList.push({
          type: 'lw-btn',
          cursor: 'ew-resize', // 左右
          id: item.id,
          color: _color
        })
        this._ctx.fillStyle = _color
        this._ctx.fillRect(item.left - w/2, item.top + item.height/2 - w/2, w, w)


        // 中右
        this.ctx.fillStyle = 'white'
        this.ctx.fillRect(item.left + item.width - w/2, item.top + item.height/2 - w/2, w, w)

        // 绘制模拟canvas
        _color = this.getRandomColor()
        this._tcList.push({
          type: 'lw-btn',
          cursor: 'ew-resize', // 左右
          id: item.id,
          color: _color
        })
        this._ctx.fillStyle = _color
        this._ctx.fillRect(item.left + item.width - w/2, item.top + item.height/2 - w/2, w, w)
      }

      /**
       * 
       *  缩放按钮
       * 
       * */ 

      // 左上
      this.ctx.beginPath()
      this.ctx.arc(item.left, item.top, r, 0, 2 * Math.PI)
      this.ctx.stroke()
      this.ctx.fillStyle = 'white'
      this.ctx.fill()

      // 绘制模拟canvas
      _color = this.getRandomColor()
      this._tcList.push({
        type: 's-btn',
        cursor: 'nwse-resize', // 左上角
        id: item.id,
        color: _color
      })
      this._ctx.beginPath()
      this._ctx.arc(item.left, item.top, r, 0, 2 * Math.PI)
      this._ctx.stroke()
      this._ctx.fillStyle = _color
      this._ctx.fill()


      // 右上
      this.ctx.beginPath()
      this.ctx.arc(item.left + item.width, item.top, r, 0, 2 * Math.PI)
      this.ctx.stroke()
      this.ctx.fillStyle = 'white'
      this.ctx.fill()

      // 绘制模拟canvas
      _color = this.getRandomColor()
      this._tcList.push({
        type: 's-btn',
        cursor: 'nesw-resize', // 右上角
        id: item.id,
        color: _color
      })
      this._ctx.beginPath()
      this._ctx.arc(item.left + item.width, item.top, r, 0, 2 * Math.PI)
      this._ctx.stroke()
      this._ctx.fillStyle = _color
      this._ctx.fill()


      // 左下
      this.ctx.beginPath()
      this.ctx.arc(item.left, item.top + item.height, r, 0, 2 * Math.PI)
      this.ctx.stroke()
      this.ctx.fillStyle = 'white'
      this.ctx.fill()

      // 绘制模拟canvas
      _color = this.getRandomColor()
      this._tcList.push({
        type: 's-btn',
        cursor: 'nesw-resize', // 左下角
        id: item.id,
        color: _color
      })
      this._ctx.beginPath()
      this._ctx.arc(item.left, item.top + item.height, r, 0, 2 * Math.PI)
      this._ctx.stroke()
      this._ctx.fillStyle = _color
      this._ctx.fill()


      // 右下
      this.ctx.beginPath()
      this.ctx.arc(item.left + item.width, item.top + item.height, r, 0, 2 * Math.PI)
      this.ctx.stroke()
      this.ctx.fillStyle = 'white'
      this.ctx.fill()

      // 绘制模拟canvas
      _color = this.getRandomColor()
      this._tcList.push({
        type: 's-btn',
        cursor: 'nwse-resize', // 右下角
        id: item.id,
        color: _color
      })
      this._ctx.beginPath()
      this._ctx.arc(item.left + item.width, item.top + item.height, r, 0, 2 * Math.PI)
      this._ctx.stroke()
      this._ctx.fillStyle = _color
      this._ctx.fill()


      this.ctx.restore()
    },

5. Determine the click area

    // 鼠标开始点击
    wrapStart(e, _isRightClick = false) {

      // 右键点击
      if(_isRightClick) {
        console.log('右键点击')
        this.mousePos = {
          x: e.clientX,
          y: e.clientY
        }

        this.showRmenu = true
      }

      // 判断画板上的点击区域
      const mousePos = {
        x: (e.pageX - this.toucheWrap.left) * this.sDpi,
        y: (e.pageY - this.toucheWrap.top) * this.sDpi
      }

      const pixel = this._ctx.getImageData(mousePos.x, mousePos.y, 1, 1).data
      const color = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`

      for (let i = 0; i < this._tcList.length; i++) {
        if (this._tcList[i].color == color) {
          const id = this._tcList[i].id
          // 缩放按钮
          if (this._tcList[i].type == 's-btn') {
            this.oTouchStart(e, id)
          } 
          // 拉伸(高度)按钮
          else if(this._tcList[i].type == 'lh-btn') {
            this.oLwhStart(e, id, 'h')
          } 
          // 拉伸(宽度)按钮
          else if(this._tcList[i].type == 'lw-btn') {
            this.oLwhStart(e, id, 'w')
          }
          else {
            // 点击图层
            this.wraptouchStart(e, id, _isRightClick)
          }

          break
        }
      }

      this.drag = true;
    },
    // 鼠标放开
    wrapUp(e) {
      // console.log("wrapUp");
      this.drag = false;
      this.oTouchUp();

      this.xMLine = false;
      this.yMLine = false;
    },
    // 鼠标取消
    oTouchUp(e, id) {
      if (this.items && this.items[this.index]) {
        // console.log("oTouchUp");
        this.items[this.index].lMoveabled = false;
        this.items[this.index].sMoveabled = false;
        this.items[this.index].moveabled = false;
      }
    },
    // 点击图层
    wraptouchStart(e, id, _isRightClick) {

      console.log("点击图层", e, e.target.focus);

      // 循环图片数组获取点击的图片信息
      for (let i = 0; i < this.items.length; i++) {
        this.items[i].active = false;
        this.items[i].moveabled = false;
        this.items[i].sMoveabled = false;

        if (id == this.items[i].id) {

          // 不是编辑封面的时候
          if(!this.isCoverEdit) {

            this.selectSpPartIndex = this.spPartIndex
            this.selectItemId = id
          }

          this.canvas.style.cursor = 'move'

          this.index = i
          this.items[this.index].active = true
          this.items[this.index].moveabled = true

          if (this.items[this.index].isEdit) {
            e.stopPropagation()
          } else {
            e.preventDefault()
          }
        }
      }

      let editType = "";

      if (this.items[this.index].type == "bg") {
        editType = "bg";
      } else if (this.items[this.index].type == "ai") {
        editType = "ai";
      } else if (this.items[this.index].type == "tt") {
        editType = "tt";
      } else if (this.items[this.index].type == "bt") {
        editType = "bt";
      } else if (this.items[this.index].type == "zm") {
        editType = "zm";
      }

      if (this.isCoverEdit) {
        this.fmItems = this.items;
      } else {
        this.spPart[this.spPartIndex] = this.items;
      }

      this.editType = editType;
      this.isInitEdit = false;

      // 获取点击的坐标值
      this.items[this.index].lx = e.pageX;
      this.items[this.index].ly = e.pageY;

      this.items = JSON.parse(JSON.stringify(this.items));

    },

 

6. Move layers

    // 鼠标移动
    wrapMove(e) {
      if (!this.canvas || !this.items[this.index]) {
        return
      }

      // 判断画板上的点击区域
      const mousePos = {
        x: (e.pageX - this.toucheWrap.left) * this.sDpi,
        y: (e.pageY - this.toucheWrap.top) * this.sDpi
      }

      const pixel = this._ctx.getImageData(mousePos.x, mousePos.y, 1, 1)?.data
      const color = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`

      if (!this.items[this.index]?.sMoveabled && !this.items[this.index]?.moveabled && !this.items[this.index]?.lMoveabled) {
        this.canvas.style.cursor = 'auto'

        for (let i = 0; i < this._tcList.length; i++) {
          if (this._tcList[i].color == color) {
            const id = this._tcList[i].id
            // 编辑按钮
            if (['s-btn', 'lh-btn', 'lw-btn'].includes(this._tcList[i].type)) {
              this.canvas.style.cursor = this._tcList[i].cursor
            }
            else {
              // 循环图片数组获取点击的图片信息
              for (let j = 0; j < this.items.length; j++) {
                if (this.items[j].id == id && this.items[j].active) {
                  if(this.items[j].type !== 'zm') {
                    this.canvas.style.cursor = 'move'
                  } else {
                    this.canvas.style.cursor = 'auto'
                  }
                  break
                }
              }
            }

            break
          }
        }
      }


      if (this.drag && this.items[this.index].type != "bg") {

        if (this.items[this.index].active) {
          // 缩放
          if (this.items[this.index].sMoveabled) {
            console.log("wrapMove-sMoveabled");
            this.oTouchMove(e);
          }
          // 移动
          else if (this.items[this.index].moveabled) {
            if(this.items[this.index].type !== 'zm') {
              console.log("wrapMove-moveabled")
              this.wraptouchMove(e)
            }
          }
          // 拉伸
          else if (this.items[this.index].lMoveabled) {
            console.log("wrapMove-lMoveabled");
            this.oLwhMove(
              e,
              this.items[this.index].id,
              this.items[this.index].lMoveType
            );
          }
        }
      }

    },
    // 拖动图层
    wraptouchMove(e) {
      let { items, index } = this;

      if (!items[index].moveabled) {
        return;
      }

      console.log("拖动图层", e);
      

      items[index]._lx = e.pageX;
      items[index]._ly = e.pageY;

      let _getXDistancs = null
      let _getYDistancs = null

      if (Math.round(items[index].x) == this.realResolutionPower[0] / 2) {
        this.xMLine = true;
        _getXDistancs = this.getDistancs(
          items[index].lx,
          items[index].ly,
          items[index]._lx,
          items[index]._ly
        )
      } else {
        this.xMLine = false;
      }


      if (Math.round(items[index].y) == this.realResolutionPower[1] / 2) {
        this.yMLine = true;
        _getYDistancs = this.getDistancs(
          items[index].lx,
          items[index].ly,
          items[index]._lx,
          items[index]._ly
        )
        
      } else {
        this.yMLine = false;
      }

      if((_getXDistancs != null && _getXDistancs < 20) || (_getYDistancs != null && _getYDistancs < 20)){
        return
      }

      items[index].left += (items[index]._lx - items[index].lx) * this.sDpi;
      items[index].top += (items[index]._ly - items[index].ly) * this.sDpi;
      items[index].x += (items[index]._lx - items[index].lx) * this.sDpi;
      items[index].y += (items[index]._ly - items[index].ly) * this.sDpi;

      

      items[index].lx = e.pageX;
      items[index].ly = e.pageY;

      this.items = JSON.parse(JSON.stringify(items))
      if (this.isCoverEdit) {
        this.fmItems = this.items;
      } else {
        this.spPart[this.spPartIndex] = this.items

        this.spPart = JSON.parse(JSON.stringify(this.spPart))
      }

    },

7. Layer scaling

    // 点击伸缩图标
    oTouchStart(e, id) {
      console.log("点击伸缩图标", e);

      // 找到点击的那个图片对象,并记录
      for (let i = 0; i < this.items.length; i++) {
        this.items[i].active = false;
        this.items[i].moveabled = false;
        this.items[i].sMoveabled = false;

        if (id == this.items[i].id) {
          this.index = i;
          this.items[i].active = true;
          this.items[i].sMoveabled = true;
        }
      }

      // 获取作为移动前的坐标
      this.items[this.index].tx = (e.pageX - this.toucheWrap.left) * this.sDpi;
      this.items[this.index].ty = (e.pageY - this.toucheWrap.top) * this.sDpi;

      // 获取图片半径
      this.items[this.index].r = this.getDistancs(
        this.items[this.index].x,
        this.items[this.index].y,
        this.items[this.index].tx,
        this.items[this.index].ty
      );

    },

    // 移动伸缩图标
    oTouchMove(e) {
      console.log("移动伸缩图标", e);

      let { items, index, toucheWrap, sDpi } = this;

      if (!items[index].sMoveabled) {
        return;
      }

      // 记录移动后的位置
      items[index]._tx = (e.pageX - toucheWrap.left) * this.sDpi;
      items[index]._ty = (e.pageY - toucheWrap.top) * this.sDpi;

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


      let _s = items[index].disPtoO / items[index].r

      // 记录新旧数据
      let _oiW = items[index].width
      let _niW = _oiW * _s

      let _oiS = _niW / _oiW

      let _oiH = items[index].height
      let _niH = _oiS * _oiH

      // 使用缩放
      // items[index].scale = items[index].disPtoO / items[index].r;

      // 不使用缩放
      if (items[index].type === "bt" || items[index].type === "zm") {
        let _newFontSize = items[index].fontSize * _s

        let maxFontSize = items[index].type == 'zm' ? 46 : 100;
        let minFontSize = 12
        
        if(_newFontSize < minFontSize) {
          return
        } else if(_newFontSize > maxFontSize) {
          return
        } 

        let _txt = TextNode(
          items[index].type === 'zm' ? '智能字幕' : items[index].content, 
          `${_newFontSize}px ${items[index].fontFamily}`
        )
        
        _niW = _txt.width
        _niH = _txt.height

        items[index].fontSize = _newFontSize;

      }

      
      if(items[index].type === 'zm') {
        // 距离底部间距
        let _distance = this.realResolutionPower[1] * 0.1

        items[index].height = _niH

        // 中心坐标
        items[index].y = this.realResolutionPower[1] - _distance - (items[index].height/2)

        items[index].top = items[index].y - items[index].height / 2

      } else {

        items[index].width = _niW
        items[index].height = _niH
  
        items[index].top = items[index].y - items[index].height / 2
        items[index].left = items[index].x - items[index].width / 2
      }


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

      this.items = JSON.parse(JSON.stringify(items));
      if (this.isCoverEdit) {
        this.fmItems = this.items;
      } else {
        this.spPart[this.spPartIndex] = this.items

        this.spPart = JSON.parse(JSON.stringify(this.spPart))
      }

    },

8. Stretch width and height

    // 点击文字拉宽高图标
    oLwhStart(e, id, _type) {
      // 找到点击的那个图片对象,并记录
      for (let i = 0; i < this.items.length; i++) {
        this.items[i].active = false;
        this.items[i].moveabled = false;
        this.items[i].sMoveabled = false;
        this.items[i].lMoveabled = false;

        if (id == this.items[i].id) {
          this.index = i;
          this.items[i].active = true;
          this.items[i].lMoveabled = true;
          this.items[i].lMoveType = _type;
        }
      }

      // 获取作为移动前的坐标
      this.items[this.index].tx = (e.pageX - this.toucheWrap.left) * this.sDpi;
      this.items[this.index].ty = (e.pageY - this.toucheWrap.top) * this.sDpi;

      // 获取触摸点到圆心距离
      this.items[this.index].r = this.getDistancs(
        this.items[this.index].x,
        this.items[this.index].y,
        this.items[this.index].tx,
        this.items[this.index].ty
      );

    },

    // 移动文字拉宽高图标
    oLwhMove(e, id, _type) {
      console.log('移动文字拉宽高图标', e)

      let { items, index, toucheWrap } = this;

      if (!items[index].lMoveabled) {
        return;
      }

      // 记录移动后的位置
      items[index]._tx = (e.pageX - toucheWrap.left) * this.sDpi
      items[index]._ty = (e.pageY - toucheWrap.top) * this.sDpi

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

      let _s = items[index].disPtoO / items[index].r;

      // 拉宽度
      if (_type === "w") {
        let _sW = _s * items[index].width

        if(['zm', 'bt'].includes(items[index].type)){
          let _txt = TextNode(
            items[index].type === 'zm' ? '智能字幕' : items[index].content, 
            `${items[index].fontSize}px ${items[index].fontFamily}`, 
            '', 
            items[index].height+'px'
          )
          
          if(_txt.width >= _sW) {
            return
          }
        }

        items[index].width = _sW
        items[index].left = items[index].x - items[index].width / 2;
      } 
      // 拉高度
      else {

        let _sH = _s * items[index].height

        if(['zm', 'bt'].includes(items[index].type)){
          let _txt = TextNode(
            items[index].type === 'zm' ? '智能字幕' : items[index].content,  
            `${items[index].fontSize}px ${items[index].fontFamily}`, 
            items[index].width+'px', 
            ''
            )
          
          if(_txt.height >= _sH) {
            return
          }
        }

        items[index].height = _sH

        items[index].top = items[index].y - items[index].height / 2;
      }

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

      this.items = JSON.parse(JSON.stringify(items))
      if (this.isCoverEdit) {
        this.fmItems = this.items
      } else {
        this.spPart[this.spPartIndex] = this.items

        this.spPart = JSON.parse(JSON.stringify(this.spPart))
      }

    },

9. Rotate layers

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

      // 获取作为移动前角度的坐标
      this.items[this.index].tx = e.layerX - this.toucheWrap.left;
      this.items[this.index].ty = e.layerY - this.toucheWrap.top;

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

    // 移动旋转图标
    oScaleMove(e, id) {
      console.log('移动旋转图标')
      let { items, index } = this;

      // 记录移动后的位置
      items[index]._tx = e.layerX - this.toucheWrap.left;
      items[index]._ty = e.layerY - this.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.layerX - this.toucheWrap.left;
      items[index].ty = e.layerY - this.toucheWrap.top;

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

      this.items = items;
      if (this.isCoverEdit) {
        this.fmItems = items;
      } else {
        this.spPart[this.spPartIndex] = items;
      }

    },

10. Calculate the distance from the coordinate point to the center of the circle

    getDistancs(cx, cy, pointer_x, pointer_y) {
      var ox = pointer_x - cx;
      var oy = pointer_y - cy;
      return Math.sqrt((ox * ox) + (oy * oy));
    },

11. Angle from the clicked coordinates to the center of the circle

    /*
     * 参数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;
    }

12. Drag and drop

<div class="image" 
    @dragstart="dragStart($event, item)" 
    draggable="true" 
    ref="boothRef">
</div>


<script>
    /* 拖动时触发 */
    dragStart($event, item) {
      console.log("开始拖动:", item);

      $event.dataTransfer.setData(
        "item",
        JSON.stringify({
          eType: "bg",
          type: item.resourceType,
          url: item.src,
          key: item.iobsKey
        })
      );
    },
</script>
<div 
      class="wrap"
      :class="{
        wrapfc: isFullscreen
      }"
      id="winBox" 
      ref="winBox"
      @click="canvasFullscreen(false)"
      @drop="drop($event)"
      @dragover="allowDrop($event)"
      @contextmenu.prevent="rightClick">

    ...
</div>

<script>

     /**
     * 拖动完成时触发
     */
    allowDrop($event) {
      $event.preventDefault();
    },

    /**
     * 拖动放置
     */
    drop($event) {
      $event.preventDefault();
      let data = $event.dataTransfer.getData("item");

      console.log('拖动放置:

Guess you like

Origin blog.csdn.net/qq_31851435/article/details/132479656