基于Vue3+LeaderLine实现画线测距及线条自由调整

1. 基于Vue3+LeaderLine实现画线测距及线条自由调整

先看下效果:我们画线后可以根据比例关系自动计算距离,并且线条不对可以自由调整
在这里插入图片描述

<template>
  <div id="image-detail">
    <el-image :src="myImageUrl" style="height: auto; width: 800px;" fit="scale-down" ref="canvasRef" @mousedown="startDrawing" @mousemove="drawLine" @mouseup="stopDrawing">
      <template #error>
        <div>
          <el-icon></el-icon>
        </div>
      </template>
    </el-image>
  </div>
</template>

<script>
import LeaderLine from 'leader-line'
import $ from 'jquery'

export default {
  ...
  methods: {
    startDrawing(event) {
      console.log("当前坐标点:" + event.offsetX + " " + event.offsetY)
      if (this.checkIfAiPoint(event)) return
    },
    drawLine(event) {
      const self = this
      if (self.ai.isAiMoving) {
        self.aiMoving(event)
        return false
      }
    },
    stopDrawing(event) {
      if (this.checkIfAiPoint(event)) return
    },
    // AI结果画图
    markPointPlus(offsetX, offsetY, tag, nameIndex) {
      const self = this
      if (document.getElementById("image-detail") === null) {
        return
      }
      
      // 点击多个点,生成多个值
      let div = document.createElement("div");
      div.className = "marker";

      div.id = "marker" + nameIndex + 'ai' + tag;
      div.style.width = 0
      div.style.height = 0
      // 红点颜色
      div.style.backgroundColor = "red";
      div.style.zIndex = 999;
      div.style.left = offsetX + "px";
      let imageHeight = $('#image-detail').height()
      div.style.top = (offsetY - imageHeight) + "px";
      div.style.position = "relative";
      // 边框半径百分比
      div.style.borderRadius = "50%";
      // 该位置监听的偏移量有问题,需计算
      // div.onclick = function (event) {
      //   // 计算出相对图片的偏移量
      //   console.log($('#image-detail .el-image__inner').left)
      //   console.log($('#image-detail .el-image__inner').top)
      //   const offsetX = event.clientX - $('#image-detail .el-image__inner').left
      //   const offsetY = event.clientY - $('#image-detail .el-image__inner').top
      //   if (self.ai.isAiMoving) {
      //     // 停止移动
      //     self.stopAiMoving(offsetX, offsetY, nameIndex, tag)
      //   } else {
      //     // 开始移动
      //     self.startAiMoving(offsetX, offsetY, nameIndex, tag)
      //   }
      // },
      document.getElementById("image-detail").appendChild(div);
      self.aiLineStartMiddleInfo[nameIndex + tag] = {'offsetX': offsetX, 'offsetY': offsetY}
    },
    startAiMoving(offsetX, offsetY, nameIndex, tag) {
      const self = this
      // 说明鼠标刚刚从原来位置up,不需要开始
      if (offsetX === self.lastAiX && offsetY === self.lastAiY) {
        return false
      }
      self.ai.isAiMoving = true
      self.ai.nameIndex = nameIndex
      self.ai.tag = tag
      self.lastAiX = offsetX
      self.lastAiY = offsetY
    },
    aiMoving(event) {
      const self = this
      const nameIndex = self.ai.nameIndex
      let imageHeight = $('#image-detail').height()
      if (!this.ai.isAiMoving) return;
      // const len = Object.keys(self.aiDrawInfo).length
      // 由于之前在开始结束位置新增了点的宽高均为4导致
      // 解决开始端移动点上移问题,原因未知, 由多点规律推断而来
      // if (self.ai.tag === 'start') {
      //   $('#marker' + nameIndex + 'ai' + self.ai.tag).css({
      //     'left': event.offsetX,
      //     'top': (event.offsetY + 4 * (len - nameIndex) * 2 - imageHeight)
      //   })
      // } else {
      //   $('#marker' + nameIndex + 'ai' + self.ai.tag).css({
      //     'left': event.offsetX,
      //     'top': (event.offsetY + 4 * ((len - nameIndex) * 2 - 2) - imageHeight)
      //   })
      // }
      $('#marker' + nameIndex + 'ai' + self.ai.tag).css({
        'left': event.offsetX,
        'top': (event.offsetY - imageHeight)
      })
      const lineRef = self.aiDrawInfo['marker' + nameIndex + 'ai']
      lineRef.position()
    },
    stopAiMoving(currOffsetX, currOffsetY, nameIndex, tag) {
      const self = this
      nameIndex = Number(nameIndex)
      // 说明鼠标刚刚从原来位置up,不需要结束
      if (currOffsetX === self.lastAiX && currOffsetY === self.lastAiY) {
        return false
      }
      self.ai.isAiMoving = false
      self.ai.nameIndex = 0
      self.ai.tag = ''
      let offsetX
      let offsetY
      if (tag === 'start') {
        offsetX = self.aiLineStartMiddleInfo[nameIndex + 'middle']['offsetX']
        offsetY = self.aiLineStartMiddleInfo[nameIndex + 'middle']['offsetY']
      } else {
        offsetX = self.aiLineStartMiddleInfo[nameIndex + 'start']['offsetX']
        offsetY = self.aiLineStartMiddleInfo[nameIndex + 'start']['offsetY']
      }
      const currDistance = Math.sqrt((currOffsetX - offsetX) ** 2 + (currOffsetY - offsetY) ** 2)
      let currRealDistance = (self.rulerRealDistance * 100 / self.rulerDistance * currDistance / 100).toFixed(1)
      // 判断是否为标尺移动
      if (nameIndex === 0) {
        self.rulerDistance = currDistance
        // 重新计算其他所有的画线距离
        this.recalculateAiByRuler()
      } else {
        const lineRef = self.aiDrawInfo['marker' + nameIndex + 'ai']
        lineRef.setOptions({'endLabel': currRealDistance + 'mm'})
      }++-
      // 同时更新坐标信息
      self.aiLineStartMiddleInfo[nameIndex + tag]['offsetX'] = currOffsetX
      self.aiLineStartMiddleInfo[nameIndex + tag]['offsetY'] = currOffsetY
      self.lastAiX = currOffsetX
      self.lastAiY = currOffsetY
    },
    // 重新计算其他所有的画线距离
    recalculateAiByRuler() {
      const self = this
      for (let nameIndex = 0; nameIndex < Object.keys(this.aiLineStartMiddleInfo).length / 2; nameIndex++) {
        // 标尺信息不重新计算
        if (nameIndex === 0 || nameIndex === 5) {
          continue
        }
        const middleOffsetX = this.aiLineStartMiddleInfo[nameIndex+'middle']['offsetX']
        const middleOffsetY = this.aiLineStartMiddleInfo[nameIndex+'middle']['offsetY']
        const startOffsetX = this.aiLineStartMiddleInfo[nameIndex+'start']['offsetX']
        const startOffsetY = this.aiLineStartMiddleInfo[nameIndex+'start']['offsetY']
        const currDistance = Math.sqrt(( middleOffsetX - startOffsetX) ** 2 + (middleOffsetY - startOffsetY) ** 2)
        let currRealDistance = ( this.rulerRealDistance * 100 / this.rulerDistance * currDistance / 100).toFixed(1)
        const lineRef = self.aiDrawInfo['marker' + nameIndex + 'ai']
        lineRef.setOptions({'endLabel': currRealDistance + 'mm'})
      }
    },
    drawAiLine(color, x1, y1, x2, y2, index, tag, rulerRealDistance) {
      const self = this
      const nameIndex = self.aiIndex
      // 由于图片自适应,等比例缩放
      let imageHeight = $('#image-detail').height()
      let imageWidth = $('#image-detail .el-image__inner').width()
      const widthRatio = self.other.aiWidth / imageWidth
      const heightRatio = widthRatio
      x1 /= widthRatio
      y1 /= heightRatio
      x2 /= widthRatio
      y2 /= heightRatio
      this.markPointPlus(x1, y1, 'start', nameIndex)
      this.markPointPlus(x2, y2, 'middle', nameIndex)
      const dom31 = document.getElementById('marker' + nameIndex + 'ai' + "start")
      const dom32 = document.getElementById('marker' + nameIndex + 'ai' + "middle")
      let lineRef3 = new LeaderLine(dom31, dom32, {'color': color, 'path': 'straight'});
      lineRef3.show()
      self.aiDrawInfo['marker' + nameIndex + 'ai'] = lineRef3
      const currDistance = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
      const currRealDistance = (rulerRealDistance * 100 / self.rulerDistance * currDistance / 100).toFixed(1)
      lineRef3.setOptions({'endLabel': currRealDistance + 'mm'})
      self.$emit('updateResult', {'index': index, 'tag': tag, 'value': currRealDistance})
    },
    drawByAi() {
      ...
    }
  }
}
</script>

<style lang="scss" scoped>
.line-block {
  height: 90vh;

  .one {
    display: flex;
    justify-content: space-around;
    // height: 100px;
  }

  .two {
    margin-top: 100px;
    display: flex;
    justify-content: space-around;
  }

  .iframe-block {
    margin-top: 50px;
  }

  .target {
    display: inline-block;
    background-color: #9ee7ea;
    padding: 12px;
  }

  .block-one {
    border: 1px solid blue;
    padding: 10px;
  }

  .block-two {
    border: 1px solid blue;
    padding: 10px;
  }

  .block-three {
    border: 1px solid blue;
    padding: 10px;
  }

  .block-four {
    border: 1px solid blue;
    padding: 10px;
  }
}
</style>

2. 基于Vue3+LeaderLine实现画线时对应点放大精确选点功能

我们在画线时,可能开始结束位置选择不准,导致测量结果会有偏差,所以新增放大功能,如下所示

核心源码:

<template>
  ...
      <el-col :span="2" v-if="largeImageSelected">
        <!-- 显示在右侧的放大之后的区域 -->
                <div class="large" v-if="selectedInsoleUrl" :style="[{'backgroundImage':'url('+selectedInsoleUrl+')'}, {'background-position-x': + -positionX*(800/400)+200 + 'px'}, {'background-position-y': + -positionY*(1066/711)+210 +'px'}]" style="text-align: center; color: red; vertical-align: middle">
                  <div style="margin-top:200px">+</div>
                </div>
      </el-col>
...
</template>

<script>
import $ from 'jquery'
import {useMouseInElement} from "@vueuse/core";

export default {
  ...
  setup () {
    const curIndex = ref(0)
    function mouseenter (index) {
      curIndex.value = index
    }
    const target = ref(null)

    const { elementX, elementY, isOutside } = useMouseInElement(target)
    // 监听
    const left = ref(0)
    const top = ref(0)
    // 定位位置
    const positionX = ref(0)
    const positionY = ref(0)
    watch([elementX, elementY], () => {
      positionX.value = elementX.value
      positionY.value = elementY.value
    })
    return {
      curIndex,
      mouseenter,
      target,
      left,
      top,
      isOutside,
      positionX,
      positionY
    }
  },
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss">
.large {
  position: absolute;
  top: 0;
  right: 12px;
  width: 400px;
  height: 400px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  background-repeat: no-repeat;
  background-size: 1600px 1600px;
  background-color: #f8f8f8;
}
.el-upload-dragger {
  height: 100%;
}
</style>
  • 其中使用useMouseInElement来实时获取当前鼠标位置并返回坐标点positionX、positionY
  • 对于位置映射如-positionY*(1066/711)+210,则可以点击原图Y开始结束位置打印当前坐标点计算距离,然后根据原图结束位置,在实际放大后显示的位置反找原图位置即可

3. 基于Vue3+LeaderLine实现鼠标点击移动画线效果

3.1 实现效果如图所示

在这里插入图片描述

3.2 技术栈

Vue3 + LeaderLine

3.3 主要代码

<template>
  <div id="image-detail">
    <el-image :src="imageUrl" style="height: 800px; width: 600px" ref="canvasRef" @mousedown="startDrawing" @mousemove="drawLine" @mouseup="stopDrawing"/>
  </div>
</template>

<script>
import LeaderLine from 'leader-line'
import $ from 'jquery'

export default {
  components: {},
  data() {
    return {
      imageUrl: 'https://picsum.photos/800/600',
      isDrawing: false,
      lastX: '',
      lastY: '',
      lineRef: null,
      lines: [],
      index: 0
    }
  },
  methods: {
    markPoint(offsetX, offsetY, tag) {
      let nameIndex = this.index;
      // 点击多个点,生成多个值
      let div = document.createElement("div" + nameIndex + tag);
      div.className = "marker";
      div.id = "marker" + nameIndex + tag;
      if (tag !== 'middle') {
        // 红点的宽
        div.style.width = "5px";
        // 红点的高
        div.style.height = "5px";
      } else {
        div.style.width = 0
        div.style.height = 0
      }
      // 红点颜色
      div.style.backgroundColor = "red";
      div.style.left = offsetX + "px";
      div.style.top = offsetY + "px";
      div.style.position = "absolute";
      // 边框半径百分比
      div.style.borderRadius = "50%";
      document.getElementById("image-detail").appendChild(div);
    },
    startDrawing(event) {
      if (this.isDrawing) return;
      this.isDrawing = true;
      this.lastX = event.clientX;
      this.lastY = event.clientY;
      this.markPoint(this.lastX, this.lastY, 'start')
    },
    drawLine(event) {
      if (!this.isDrawing) return;
      let nameIndex = this.index
      let dom1 = document.getElementById('marker' + nameIndex + 'start')
      let dom2 = document.getElementById('marker' + nameIndex + "middle")
      if (dom2 === null) {
        this.markPoint(event.clientX, event.clientY, 'middle')
        dom2 = document.getElementById('marker' + nameIndex + "middle")
        this.lineRef = new LeaderLine(dom1, dom2,
            {'color': 'red', 'path': 'straight'});
        this.lineRef.show()
      } else {
        $('#marker' + nameIndex + "middle").css({'left': event.clientX, 'top': event.clientY})
        this.lineRef.position()
      }
    },
    stopDrawing(event) {
      if (!this.isDrawing) return;
      let nameIndex = this.index
      $('#marker' + nameIndex + "middle").css({'left': event.clientX, 'top': event.clientY})
      // TODO 计算距离
      this.lineRef.setOptions({'endLabel': 'end'})
      this.lineRef.position()
      this.isDrawing = false;
      if (this.lineRef) {
        this.lines.push(this.lineRef);
      }
      this.index++
    }
  }
}
</script>

<style lang="scss" scoped>
</style>

3.4 针对报错处理

vue.config.js中配置如下

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  configureWebpack: config => {
    let path = require('path')
    config.module.rules.push({
      test: path.resolve(__dirname, 'node_modules/leader-line/'),
      use: [
        {
          // 解决leader-line报错问题
          loader: 'skeleton-loader',
          options: {
            procedure: content => `${content}export default LeaderLine`
          }
        }
      ]
    })
  }
})

欢迎关注公众号算法小生与我沟通交流

欢迎关注公众号 算法小生

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
你好!对于使用Vue 3、TypeScript和Vite来实现一个看房自由的应用,可以结合Three.js这个D图形库来实现。下面是一个简单的步骤指南: 1. 首先,确保你已经安装了Node.js和npm。 2. 创建一个新的Vue项目,可以使用Vue CLI来快速搭建一个基本的项目结构。 ```bash npm install -g @vue/cli vue create my-project ``` 3. 在Vue项目中安装Vite作为开发服务器。 ```bash cd my-project npm install -D create-vite npx create-vite ``` 4. 安装Three.js库和相关依赖。 ```bash npm install three ``` 5. 在Vue组件中引入Three.js库,并开始编写代码来实现看房自由功能。 ```typescript <template> <div ref="container"></div> </template> <script lang="ts"> import { ref, onMounted } from 'vue'; import * as THREE from 'three'; export default { setup() { const container = ref(null); onMounted(() => { // 创建场景、相机和渲染器 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer(); // 设置渲染器的大小并将其添加到DOM中 renderer.setSize(window.innerWidth, window.innerHeight); container.value.appendChild(renderer.domElement); // 创建一个立方体并将其添加到场景中 const geometry = new THREE.BoxGeometry(); const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); const cube = new THREE.Mesh(geometry, material); scene.add(cube); // 设置相机的位置 camera.position.z = 5; // 动画循环 const animate = function () { requestAnimationFrame(animate); // 使立方体旋转起来 cube.rotation.x += 0.01; cube.rotation.y += 0.01; // 渲染场景和相机 renderer.render(scene, camera); }; animate(); }); return { container, }; }, }; </script> <style> #container { width: 100%; height: 100%; } </style> ``` 这只是一个简单的示例,你可以根据自己的需求来构建更复杂的场景和交互逻辑。希望对你有所帮助!如有任何疑问,欢迎继续提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

算法小生Đ

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

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

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

打赏作者

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

抵扣说明:

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

余额充值