【canvas】在Vue3+ts中实现 canva内的矩形拖动操作。

前言

canvas内的显示内容如何拖动?
这里提供一个 canvas内矩形移动的解决思路。
在这里插入图片描述

在这里插入图片描述

描述

如何选中canvas里的某部分矩形内容,然后进行拖动?
我的解决思路:

  1. **画布搭建。**用一个div将canvas元素包裹,设置宽高,div设置成相对定位(relative),canvas设置绝对定位(absolute)。
  2. 在往canvas内添加内容时,请保存添加内容的相关属性,长宽、位置、样式等,以此确定这部分内容的初始状态。例如:往canvas加一个矩形 ,就要先保存一下它的宽高和原点。
  3. 确定选中的内容。并与第一步保存的相关内容数据匹配。 由于canvas内添加的内容无法进行事件绑定,我们需要靠给canvas绑定点击事件,并根据点击位置确定哪部分内容被选中了。
  4. 生成可操作盒子。通过选中内容的数据,生成一个新的Dom元素盒子,并清除canvas内当前选中内容部分。给dom盒子绑定移动事件(mouse模拟拖动)。
  5. 拖动结束后,更新选中内容数据,在结束区域,canvas重新绘制

实现

1.画布搭建

   <div class="content" ref="canvasContent">
      <canvas id="canvas" ref="canvas" @click="canvasClickFn"></canvas>
    </div>
.content {
    position: relative;
    width: 800px;
    height: 600px;
  }
  #canvas {
    position: absolute;
    width: 800px;
    height: 600px;
    border: 1px solid #000;
    background-color: #fafafa;
  }

这一步要保证外层盒子和canvas大小一致。

2. 初始化canvas内容

在往canvas内添加内容时,请保存添加内容的相关属性,长宽、位置、样式等,以此确定这部分内容的初始状态。

  import { onMounted, reactive, ref, type Ref } from 'vue';
  interface DivStyle {
    boder?: string;
    backgroundColor?: string;
    width?: string;
    height?: string;
  }
  interface DiagramObj {
    id: string | number;
    path: Float32Array;
    origin: Array<number>;
    type: string;
    width?: number;
    height?: number;
    r?: number;
    style?: DivStyle;
  }
  let ctx: CanvasRenderingContext2D | null | undefined;
  const canvasContent: Ref<HTMLElement | null> = ref(null);
  const canvas: Ref<HTMLCanvasElement | null> = ref(null);
  const diagramObjArr: Array<DiagramObj> = reactive([]);
  const initCanvas = () => {
    if (canvas.value) {
      ctx = canvas.value?.getContext('2d');
      canvas.value.width = 800;
      canvas.value.height = 600;
    }
  };
  onMounted(() => {
    initCanvas();
    if (ctx) {
      let rect1 = new Float32Array([1, 1, 50, 1, 50, 30, 1, 30]);
      let rectObj = {
        id: 'rect1',
        path: rect1,
        origin: [1, 1],
        width: 50,
        height: 30,
        type: 'rect',
        style: {
          boder: '1px solid #000',
          backgroundColor: '#fff',
          width: '50px',
          height: '30px',
        },
        children: [],
      };
      drawRect(ctx, rect1);
      diagramObjArr.push(rectObj);
    }
  });
  //  绘制图形
  function drawRect(ctx: CanvasRenderingContext2D, array: Float32Array) {
    if (array.length % 2 !== 0) {
      console.error('drwaRect函数Float32Array参数长度需要偶数位');
      return;
    }
    ctx.beginPath();

    for (let i = 0; i < array.length; i += 2) {
      let x = array[i];
      let y = array[i + 1];
      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
    }
    ctx.closePath();
    ctx.stroke();
  }

rectObj是一个原点1,1;宽50,高30的盒子,然后 根据canvas路径api绘制图形。

3. 选中内容

绑定canvas点击事件,却定点击位置和点击位置下的内容。

  const canvasClickFn = (e: MouseEvent) => {
    let point = [e.offsetX, e.offsetY];
    let res = isGraphIstersection(point, diagramObjArr[0]);
    if (res && ctx) {
      console.log('在内部::', res.width);
      //  在图形正上方创建可操作图形
      createElementFn(canvasContent.value, res);
      //  清除该区域
      clearRect(ctx, [...res.origin, res.width, res.height]);
    }
  };
   function clearRect(ctx: CanvasRenderingContext2D, array: Array<number | undefined>) {
    const [x, y, width, height] = array as Array<number>;
    //  把1px 的边框算上
    ctx.clearRect(x - 1, y - 1, width + 2, height + 2);
  }
  // 圆点 和 多边形相交检测
  function isGraphIstersection(point: Array<number>, target: DiagramObj) {
    const { origin, width, height, r } = target;
    let apogee = [0, 0];
    //  求两矩形形中心点距离
    switch (target.type) {
      case 'rect':
        //  矩形  坐标轴法,不考虑矩形旋转
        if (!width || !height) return false;
        //  最远点
        apogee = [origin[0] + width, origin[1] + height];
        if (
          point[0] >= origin[0] &&
          point[0] <= apogee[0] &&
          point[1] >= origin[1] &&
          point[1] <= apogee[1]
        ) {
          return target;
        }
        return false;
      case 'circle':
        if (!r) return false;
        if (
          Math.pow(Math.abs(point[0] - origin[0]), 2) +
            Math.pow(Math.abs(point[1] - origin[0]), 2) <
          r * r
        ) {
          return target;
        }
        return false;
      case 'polygon':
        return false;
    }
  }

圆点 和 多边形相交检测 这个函数我只简单实现了矩形和圆形的检测(不考虑旋转)。如果想多检测其他的形状,需要自行实现。

4. 生成可操作盒子

根据选中的数据生成可操作盒子,盒子绑定事件,实现拖动
 function createElementFn(source: HTMLElement | null, obj: DiagramObj) {
    const { width, height, origin, style } = obj;
    if (!source || !width || !height) return;

    const div = document.createElement('div');
    div.setAttribute(
      'style',
      `
    position:absolute;
    top:${origin[1]}px;
    left:${origin[0]}px;
    width:${style?.width};
    height:${style?.height};
    border:${style?.boder};
    background-color:${style?.backgroundColor};
    box-shadow: 0px 0px 3px skyblue;
    `,
    );
    let divClickLeft = 0,
      divClickTop = 0; //  元素点击时本身偏移量
    let isStart = false;
    let finallyLeft = origin[0],
      finallyTop = origin[1]; // 最终偏移量
    div.onmousedown = (e: MouseEvent) => {
      divClickLeft = e.offsetX as number;
      divClickTop = e.offsetY as number;
      isStart = true;
    };
    div.onmousemove = (e: MouseEvent): void => {
      if (!isStart) return;
      const parentV = source.getBoundingClientRect();
      const [left, top] = [
        e.pageX - parentV.left - divClickLeft,
        e.pageY - parentV.top - divClickTop,
      ];
      if (
        left < 0 ||
        top < 0 ||
        left > parentV.width - (width as number) ||
        top > parentV.height - (height as number)
      )
        return;
      e.target.style.top = top + 'px';
      e.target.style.left = left + 'px';
      finallyLeft = left;
      finallyTop = top;
    };
    div.onmouseup = div.onmouseleave = (e: MouseEvent) => {
      if (!isStart) return;
      isStart = false;
      //  拖动好后在新区域重新绘画
      let newRectObj: DiagramObj = obj;
      let pw = finallyLeft + width;
      let ph = finallyTop + height;
      Object.assign(newRectObj, {
        path: new Float32Array([finallyLeft, finallyTop, pw, finallyTop, pw, ph, finallyLeft, ph]),
        origin: [finallyLeft, finallyTop],
      } as DiagramObj);
      if (ctx) {
        drawRect(ctx, newRectObj.path);
        let index = diagramObjArr.findIndex((item) => item.id === newRectObj.id);
        diagramObjArr.splice(index, 1, newRectObj);
        source.removeChild(div);
      }
    };

5.拖动完成后重新绘制图形

拖动完成后,在新的位置重新绘制图形。需要在生成盒子的鼠标抬起和鼠标移出实现。

div.onmouseup = div.onmouseleave = (e: MouseEvent) => {
      if (!isStart) return;
      isStart = false;
      //  拖动好后在新区域重新绘画
      let newRectObj: DiagramObj = obj;
      let pw = finallyLeft + width;
      let ph = finallyTop + height;
      Object.assign(newRectObj, {
        path: new Float32Array([finallyLeft, finallyTop, pw, finallyTop, pw, ph, finallyLeft, ph]),
        origin: [finallyLeft, finallyTop],
      } as DiagramObj);
      if (ctx) {
        drawRect(ctx, newRectObj.path);
        let index = diagramObjArr.findIndex((item) => item.id === newRectObj.id);
        diagramObjArr.splice(index, 1, newRectObj);
        source.removeChild(div);
      }
    };

效果

canvas移动


效果地址:

由于是模拟的拖动,不能拖动过快,下次想办法优化下,下次一定。

结语

结束了。 这个canvas拖动如果封装好的话,感觉是很有用的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZSK6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值