当前位置:   article > 正文

前端通过draggable结合fabricjs实现拖拽至画布生成元素自定义编排功能

前端通过draggable结合fabricjs实现拖拽至画布生成元素自定义编排功能

前端通过draggable结合fabricjs实现拖拽自定义编排功能

太久没有更新了,主要最近行情不太好失业了一段时间,一度到怀疑人生,然后就是做的东西大多没有什么含金量,没什么好分享的就很尴尬。
刚好最近遇到一个奇葩的需求,一个基地管理的需求,由于项目中的基地很偏,地图上都定位不到,只能通过一个图片作为底图,然后在上面绘制一些图层,需要做一个自定义编排的需求,先上图:
在这里插入图片描述

上面是实现的demo,首先上html结构代码(技术栈:V3+TS+elementplus)

<div class="massif-box">
          <!-- 左侧栏 -->
          <div class="asidebox">
              <div class="topbar">
                  <div
                  class="tabitem" 
                  @click="changetype(item)"
                  v-for="item in typelist" 
                  :key="item.code" 
                  :class="{cur:item.code == curtype}">
                  {{ item.name }}</div>
              </div>
              <div class="barcontent">
                  <div class="searchbar">
                      <el-input
                          v-model="searchvalue"
                          style="width:calc(100% - 20px)"
                          placeholder="请输入关键字"
                          :suffix-icon="Search"
                      />
                  </div>
                  <div class="block-content">
                      <div class="block-item" v-for="(item) in curlist" :key="item.id" :class="{cur:item.id == cur!.id}" @click.capture="selectitem(item)">
                          <div class="imgbox"></div>
                          <div class="text">{{ item.name }}</div>
                          <el-tag type="success" class="tag">陆基</el-tag>
                          <template v-if="item.id == cur.id">
                              <el-icon class="icon" @click.stop="editattr"><EditPen /></el-icon>
                              <el-icon class="icon" @click.stop="removevnode"><Delete /></el-icon>
                          </template>
                      </div>
                  </div>
                  <button @click="tojson">画布转json</button>
                  <button @click="tocanvas">json回显画布</button>
              </div>
          </div>
          <!-- 右边内容区域 -->
          <div class="basecontent" ref="basecontent" @drop="drop" @dragover="dragOver">
              <!-- 画布容器 -->
              <canvas id="canvas"></canvas>
              <!-- 可拖拽元素 -->
              <div class="toolone" @dragstart.capture="onStart">
                  <el-tooltip
                      class="box-item"
                      effect="dark"
                      content="地块"
                      placement="right"
                  >
                      <div class="item-one">
                          <img :src="massifimg" alt="" :draggable="true"/>
                      </div>
                  </el-tooltip>
                  <el-tooltip
                      class="box-item"
                      effect="dark"
                      content="塘口"
                      placement="right"
                  >
                      <div class="item-two">
                          <img :src="pondimg" alt="" :draggable="true"/>
                      </div>
                  </el-tooltip>
                  <el-tooltip
                      placement="right-start"
                      class="custom-tooltip"
                      effect="light"
                  >
                      <template #content>
                          <div class="tip-box" @dragstart.stop="onStart">
                              <div class="device-one tip-item">
                                  <img :src="video" alt="" :draggable="true"/>
                                  <div>xxx</div>
                              </div>
                              <div class="device-two tip-item">
                                  <img :src="onedevice" alt="" :draggable="true"/>
                                  <div>yyyy</div>
                              </div>
                              <div class="device-three tip-item">
                                  <img :src="video" alt="" :draggable="true"/>
                                  <div>mmmm</div>
                              </div>
                          </div>
                      </template>
                      <div class="item-three">
                          <img :src="deviceimg" alt=""/>
                      </div>
                  </el-tooltip>
              </div>
              <!-- 右下角工具元素 -->
              <div class="tooltwo">
                  <div class="top">
                      <img :src="layerimg" alt="" @click="openlyer"/>
                  </div>
                  <div class="center">
                      <img :src="daohangimg" alt="" />
                      <img :src="screenimg" alt="" />
                      <img :src="reductionimg" alt=""/>
                  </div>
                  <div class="bottom">
                      <img :src="addimg" alt="" @click="zoomIn" />
                      <img :src="minusimg" alt="" @click="zoomOut"/>
                  </div>
              </div>
              <ponddialog :pondparams="circleparams" ref="pondDialog" @get-value="updatecanvas"/>
              <massifdialog :massifparams="rectparams" ref="massifDialog" @get-value="updatecanvas"/>
              <devicedialog :deviceparams="deviceparams" ref="deviceDialog" @get-value="updatecanvas"/>
              <layerdialog :layerlist="layerlist" ref="layerDialog" @get-visible="updatevisible"/> 
          </div>
      </div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109

结构分为左侧菜单栏和右侧画布区域,通过左上角的图标拖拽到画布上生成图形,选中图形弹出属性设置框,可以调制样式或更新数据。整个画布也可以转成json存储,通过json也可以回显画布。

  import { EditPen, Plus, Delete, Search } from '@element-plus/icons-vue';
  EditPen 
  Plus  
  Delete 
  Search
  //弹框组件
  import ponddialog from './ponddialog.vue';
  import massifdialog from './massifdialog.vue';
  import devicedialog from './devicedialog.vue';
  import layerdialog from './layerdialog.vue';
  //图片
  import massifimg from'@/assets/imgs/massif/massif.png';
  import pondimg from'@/assets/imgs/massif/pond.png';
  import deviceimg from'@/assets/imgs/massif/device.png';
  import addimg from'@/assets/imgs/massif/add.png';
  import minusimg from'@/assets/imgs/massif/minus.png';
  import screenimg from'@/assets/imgs/massif/screen.png';
  import reductionimg from'@/assets/imgs/massif/reduction.png';
  import daohangimg from'@/assets/imgs/massif/daohang.png';
  import layerimg from'@/assets/imgs/massif/layer.png';
  import video from'@/assets/imgs/massif/video.png';
  import onedevice from'@/assets/imgs/massif/onedevice.png';
  import basemap from'@/assets/imgs/massif/basemap2.png';
  //画布插件
  import * as fabric from 'fabric';
  //生成唯一id方法
  import { generateUUID } from '@/utils'
  //参数类型声明
  import {Rectparams,Circleparams,DeviceParams,p, Curparams} from './types'
  // 画布区域的父级元素  
  const basecontent = ref<HTMLElement>();
  //canvas实例
  let canvas: fabric.Canvas;
  //搜索值
  let searchvalue = ref<string>('');
  
  //以下是左侧列表的相关数据
  //图层的类型
  const typelist = ref<{name:string,code:string}[]>([{name:'地块',code:'Rect'},{name:'塘口',code:'Circle'},{name:'设备',code:'Device'}]);
  //当前图层类型
  const curtype = ref<string>('Rect');
  //所有图层数组
  let vnodelist = ref<Curparams[]>([])
  //选择类型
  const changetype = (item:{name:string,code:string})=>{
      curtype.value = item.code;
  }
  //根据类型过滤出当前列表
  let curlist = computed(()=>{
      let list =  vnodelist.value.filter(item => (item.types == curtype.value && item.name.startsWith(searchvalue.value)));
      return list
  })
  
  //生命周期初始化画布
  onMounted(() => {  
      initFabricCanvas(drawbasemap);//drawbasemap是绘制地图的
  });
  
  //图层弹框数据
  let layerlist = ref<Array<fabric.Object & p >>([] as Array<fabric.Object & p >)
  watch(()=>vnodelist.value,()=>{
      layerlist.value = canvas!.getObjects() as Array<fabric.Object & p>
  },{
      deep:true
  })
  
  //画布初始化操作
  function initFabricCanvas(callback) {  
    if (!basecontent.value) return;  
    canvas = new fabric.Canvas('canvas', {  
      width: basecontent.value.offsetWidth,  
      height: basecontent.value.offsetHeight,
      preserveObjectStacking:true
    });
    callback && callback()
  }
  
  //绘制底图  地图就是最底层的假地图图片,所以需要默认先绘制
  const drawbasemap = ()=>{
      const img = new Image();  
      img.src = basemap; 
      let id = generateUUID(); 
      img.onload = () => {  
          const imgLayer = new fabric.Image(img, {  
              selectable:false,
              hasControls:false,  
              left: 0,  
              top: 0,  
              scaleX: canvas!.width / img.width,  
              scaleY: canvas!.height / img.height,  
              z: 1,
              id,
              types:'Base'  
          });  
          canvas!.add(imgLayer);
      }
  }
  
  //开始拖拽事件,根据classname判断拖拽的元素,不同的classname传递不同的type
  function onStart(e){
      let classname = ref<string>('')
      classname.value = e.target.parentElement.className.split(' ')[0];
      switch (classname.value) {
          case 'item-one':
              e.dataTransfer.setData('type', 'Rect');
              break;
          case 'item-two':
              e.dataTransfer.setData('type', 'Circle');
              break;
          case 'device-one':
              e.dataTransfer.setData('type', 'device-one');
              break;
          case 'device-two':
              e.dataTransfer.setData('type', 'device-two');
              break;
          case 'device-three':
              e.dataTransfer.setData('type', 'device-three');
              break;
          default:
              break;
      }
  }
  
  //拖拽过程中阻止默认事件
  function dragOver(e){
      e.preventDefault();
  }
  
  //拖拽完成绘制图形
  function drop(e) {
      let types = ref<string>('');
      types.value = e.dataTransfer.getData('type');//这里拿到拖拽开始事件传递过来的type
      let vnode:fabric.Object;
      let id = generateUUID();
      switch (types.value) {//根据type绘制不同的图形
          case 'Rect':
              let objone = {
                  selectable: true, // 是否可选
                  hasControls:false,
                  top:(e.pageY - (e.pageY - e.offsetY))/scale.value,
                  left:(e.pageX - (e.pageX - e.offsetX))/scale.value,//创建对象的x坐标
                  width: 150, //宽和高
                  height: 300,
                  fill:'rgba(73, 120, 236,0.6)', //填充颜色
                  stroke:'rgba(38, 162, 234,1)', //线条颜色
                  strokeWidth: 4, //线条宽度
                  strokeOpacity:0.5,
                  types:types.value,
                  id,
                  name:'地块',
                  z:2,
                  classify:'a',
                  area:2,
                  zoomX:scale.value,
                  zoomY:scale.value,
                  angle:0,
                  visible:true
              }
              vnode = new fabric.Rect(objone) // 开始绘制
              canvas!.add(vnode); //添加到画布中去
              vnodelist.value.push(objone);
          break;
          case 'Circle':
              let objtwo = {
                  selectable: true, // 是否可选
                  hasControls:false,
                  top:(e.pageY - (e.pageY - e.offsetY))/scale.value,
                  left:(e.pageX - (e.pageX - e.offsetX))/scale.value,//创建对象的x坐标
                  rx: 25,    // 圆的水平半径
                  ry: 25,    // 圆的垂直半径
                  fill: 'rgba(73, 120, 236,0.6)', // 填充颜色  
                  stroke: 'rgba(255,255,255,1)', // 描边颜色  
                  strokeWidth: 1, // 描边宽度  
                  types:types.value,
                  id,
                  name:'塘口',
                  z:3,
                  zoomX:scale.value,
                  zoomY:scale.value,
                  visible:true
              }
              vnode = new fabric.Ellipse(objtwo);
              canvas!.add(vnode);
              vnodelist.value.push(objtwo);
              break;
          case 'device-one':
              const imgone = new Image();  
              imgone.src = video;
              let oneparams = drawdevice(e,id,'视频监控');
              imgone.onload = () => {  
                  const imgerone = new fabric.Image(imgone, oneparams);  
                  canvas!.add(imgerone); 
                  vnodelist.value.push(oneparams);
              } 
              break;
          case 'device-two':
              const imgtwo = new Image();  
              imgtwo.src = onedevice;  
              let twoparams = drawdevice(e,id,'一体设备');
              imgtwo.onload = () => {  
                  const imgertwo = new fabric.Image(imgtwo,twoparams);  
                  canvas!.add(imgertwo);
                  vnodelist.value.push(twoparams);  
              } 
              break;
          case 'device-three':
              const imgthree = new Image();  
              imgthree.src = video;
              let threeparams = drawdevice(e,id,'安防视频');
              imgthree.onload = () => {  
                  const imgerthree = new fabric.Image(imgthree, threeparams);  
                  canvas!.add(imgerthree);
                  vnodelist.value.push(threeparams);
              } 
              break;
          default:
              break;
      }  
      reorderObjectsByZ()
  }
  
  //绘制设备类图层参数处理
  function drawdevice(e:DragEvent,id:string,name:string){
      return {
          selectable: true, // 是否可选
          hasControls:false,
          top:(e.pageY - (e.pageY - e.offsetY))/scale.value,
          left:(e.pageX - (e.pageX - e.offsetX))/scale.value,
          z: 4,
          id,
          name,
          refnumber:'0',
          versionid:'',
          types:'Device',
          zoomX:scale.value,
          zoomY:scale.value, 
          visible:true
      }
  }
  
  //循环画布中的元素始终保持层级z有效
  function reorderObjectsByZ() {//因为后绘制的图形层级会高一些,为了跟据z属性保持层级逻辑
      if (canvas) {  
          const objects = canvas!.getObjects().sort((a:fabric.Object & p, b:fabric.Object & p) => a.z - b.z);  //根据z属性排序
          canvas.clear(); // 移除所有现有对象  
          objects.forEach(obj => {
              canvas.add(obj); // 重新添加对象  
          });  
      }  
  } 
  
  //选中激活对应的图形
  let cur = ref<Curparams>({} as Curparams);//记录当前选中的数据
  let rectparams = ref<Rectparams>({} as Rectparams)
  let circleparams = ref<Circleparams>({} as Circleparams)
  let deviceparams = ref<DeviceParams>({} as DeviceParams)
  
  //选择图层获取参数
  function selectitem(item){
      cur.value = item;
      closeall();
      canvas!.getObjects().forEach((obj: fabric.Object & p) => {
          if(cur.value.id == obj.id){//根据唯一id判断选中的哪个元素,获取数据回填表单
              let defaultparam = {
                  id:obj.id,
                  name:obj.name,
                  types:obj.types
              }
              switch(obj.types){
                  case 'Rect':
                      let rectfill = splitRgbaSimple(obj.fill as string);
                      let rectstroke = splitRgbaSimple(obj.stroke as string);
                      rectparams.value = {
                          ...defaultparam,
                          fill:rectfill.color,
                          fillopacity:rectfill.opacity,
                          width: obj.width,  
                          height: obj.height,
                          strokeWidth: obj.strokeWidth,
                          stroke: rectstroke.color, 
                          strokeOpacity:rectstroke.opacity,
                          area:obj.area ? + obj.area : 0,
                          classify:obj.classify + '',
                          angle:obj.angle
                      };          
                  break;
                  case 'Circle':
                      let circlefill = splitRgbaSimple(obj.fill as string);
                      let circlestroke = splitRgbaSimple(obj.stroke as string);
                      circleparams.value = {
                          ...defaultparam,
                          fill:circlefill.color,
                          fillopacity:circlefill.opacity,
                          left: obj.left,  
                          top: obj.top,  
                          rx: obj.rx*2,  
                          ry: obj.ry*2,
                          strokeWidth: obj.strokeWidth,
                          stroke: circlestroke.color,
                          strokeOpacity:circlestroke.opacity,
                      };          
                  break;
                  case 'Device':
                      deviceparams.value = {  
                          ...defaultparam,
                          refnumber:obj.refnumber + '',
                          versionid:obj.versionid + '',
                      };
                  break;
              }
              canvas!.setActiveObject(obj); // 激活选中元素  
              canvas!.renderAll(); //重新渲染画布(虽然选中元素通常会自动触发重绘)
          }
      });  
  }
  
  //将rgba提取为rgb的格式和透明度
  function splitRgbaSimple(rgbaString:string) {  
      const alphaIndex = rgbaString.lastIndexOf(',');  
      const opacity = parseFloat(rgbaString.slice(alphaIndex + 1, -1)) * 100;
      const rgbString = rgbaString.slice(5, alphaIndex); 
      const color = `rgb(${rgbString.replace(/\s+/g,'')})`;
      return { color, opacity };  
  } 
  
  //弹框实例
  const pondDialog = ref();
  const massifDialog = ref();
  const deviceDialog = ref();
  const layerDialog = ref();
  
  //打开修改属性弹框
  function editattr(){
      let mapflag = {
          'Rect': massifDialog,
          'Circle': pondDialog,
          'Device': deviceDialog,
      }
      mapflag[cur.value.types].value.disbled = true;
  }
  
  //回填数据点击确定更新图层
  function updatecanvas(params:any){
      console.log(params);
      canvas!.getObjects().forEach((obj: fabric.Object & p) => {
          if(cur.value.id == obj.id){            
              switch(obj.types){
                  case 'Rect':
                      obj.set({
                          ...params,
                          width:params.width ? +params.width : 50,
                          height:params.height ? +params.height : 100,
                          stroke:rgbToRgba(params.stroke,params.strokeOpacity),
                          fill:rgbToRgba(params.fill,params.fillopacity),
                          angle:+params.angle
                      });
                  break;
                  case 'Circle':
                      obj.set({
                          ...params,
                          rx:params.rx ? (+params.rx)/2 : 25,
                          ry:params.ry ? (+params.ry)/2 : 25,
                          left:+params.left,
                          top:+params.top,
                          stroke:rgbToRgba(params.stroke,params.strokeOpacity),
                          fill:rgbToRgba(params.fill,params.fillopacity)
                      });
                  break;
                  case 'Device':
                      obj.set(params);
                  break;
              }
              updatelist({name:obj.name,id:obj.id,types:obj.types})//更新列表中的数据
              canvas!.requestRenderAll(); // 重新渲染画布(虽然选中元素通常会自动触发重绘)
          }
      }); 
  }
  
  //处理颜色格式 最终显示是rgba的格式
  function rgbToRgba(rgbString, alpha) {  
    const rgbArray = rgbString.replace(/^rgb\(([^)]+)\)$/, '$1').split(',');  
    const r = parseInt(rgbArray[0].trim(), 10);  
    const g = parseInt(rgbArray[1].trim(), 10);  
    const b = parseInt(rgbArray[2].trim(), 10);
    alpha = (parseInt(alpha) / 100).toFixed(1);
    return `rgba(${r}, ${g}, ${b}, ${alpha})`;  
  }  
  
  //控制图层弹框中选的图层控制当前元素的显示与隐藏
  function updatevisible(item){
      canvas!.getObjects().forEach((obj: fabric.Object & p) => {
          if(item.id == obj.id){//查找当前选中的元素            
              obj.visible = item.visible;
              canvas!.requestRenderAll()
          }
      });
  }
  //删除图层
  function removevnode(){
      canvas!.getObjects().forEach((obj: fabric.Object & p) => {
          if(cur!.value!.id == obj?.id){//查找当前选中的元素            
              canvas!.remove(obj);//移除元素
              removelist();//对应的左侧栏数据也移除
          }
      });  
  }
  
  //更新左侧列表数据
  function updatelist(params){    
      let i = vnodelist.value.findIndex(item => item.id == cur.value.id);
      vnodelist.value[i] = params;
  }
  
  //画布转json
  const tojson = ()=>{
      let jsonbefore = ref<{version:string,objects:Array<fabric.Object & p>}>({
          objects:[],
          version:'6.1.0'
      })
      canvas!.getObjects().forEach((obj: fabric.Object & p) => {
          let objadd = obj.toObject();
          ['id','z','name','types','selectable','hasControls','classify','area','refnumber','versionid'].forEach(item=>{
              obj[item] && (objadd[item] = obj[item])
          })
          objadd.hasControls = false;      
          jsonbefore.value.objects.push(objadd)
      });
      //调试专用
      localStorage.setItem('canvas',JSON.stringify(jsonbefore));
      canvas!.clear()
      //调用接口
      //return JSON.stringify(jsonbefore)
  }
  
  //用json回显画布
  const tocanvas = ()=>{
      let json = JSON.parse(localStorage.getItem('canvas') as string); 
      canvas!.loadFromJSON(json._value, () => {          
          canvas!.requestRenderAll();
          setTimeout(()=>{
              scale.value = canvas!.getObjects()[1].zoomX as number;
              vnodelist.value = canvas!.getObjects().map((item:fabric.Object & p) =>{
                  return {
                      id: item.id,
                      types:item.types,
                      name:item.name,
                      visible:item.visible,
                  }
              })
          },100)
      });  
  }
  
  //移除左侧列表数据
  function removelist(){
      closeall()
      let i = vnodelist.value.findIndex(item => item.id == cur.value.id);
      vnodelist.value.splice(i, 1);
  }
  
  //关闭所有弹框
  function closeall(){
      [massifDialog,pondDialog,deviceDialog,layerDialog].forEach(item =>{
          item.value.disbled = false;
      })
  }
  
  //放大缩小事件 最大放大两倍 最小还原1:1
  let scale = ref<number>(1);
  function zoomIn() {  
      if (scale.value < 2) {  
          scale.value += 0.1; // 可以调整步长来平滑缩放  
          canvas!.setZoom(scale.value)
      }  
  }
  
  //缩小
  function zoomOut() {  
      if (scale.value > 1) {  
          scale.value -= 0.1;  
          canvas!.setZoom(scale.value);
      }  
  }
  //打开图层弹框
  function openlyer(){
      closeall();
      layerDialog.value.disbled = true;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
  • 458
  • 459
  • 460
  • 461
  • 462
  • 463
  • 464
  • 465
  • 466
  • 467
  • 468
  • 469
  • 470
  • 471
  • 472
  • 473
  • 474
  • 475
  • 476
  • 477
  • 478
  • 479
  • 480
  • 481
  • 482
  • 483
  • 484
  • 485
  • 486
  • 487
  • 488

以上是全部代码,上述代码中解决了以下问题
1、拖拽是基于html5的新特性draggable结合其自带的拖拽方法拿到xy坐标,计算位于目标元素xy坐标
2、fabric画布元素的层级问题,无论元素创建的先后始终保证自定义层级有效(reorderObjectsByZ方法)
3、fabric画布转json自定义参数丢失的问题(tojson 方法)
4、fabric画布放大或缩小后xy坐标偏移的问题 (记录scale缩放比,始终计算left与top值)
5、ts中fabric画布元素类型如何兼容自定义属性(自定义P类型,与fabric.object交叉声明)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/1021158
推荐阅读
相关标签
  

闽ICP备14008679号