工业仿真(simulation)--前端(五)--标尺,刻度尺

在这篇文章中,主要讲一下标尺组件,这也是一个自定义的组件,核心是canvas,如图一

图一

这个刻度尺是和画布相结合,当画布移动时,刻度尺也会跟随移动,画布缩放时,刻度尺也会跟随缩放

话不多说,先展示核心代码

标尺组件

html 复制代码
<template>
  <canvas ref="canvasRulerRef" class="ruler-canvas"></canvas>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'

const props = defineProps({
  direction: { type: String as () => 'horizontal' | 'vertical', default: 'horizontal' },
  tickColor: { type: String, default: '#ccc' },
  textColor: { type: String, default: '#999' },
  unitSize: { type: Number, default: 100 },
  subTickCount: { type: Number, default: 5 },
  offset: { type: Number, default: 0 },
  valueScale: { type: Number, default: 100 },
  textOffsetX: { type: Number, default: 4 },
  textOffsetY: { type: Number, default: 10 }
})

const canvasRulerRef = ref<HTMLCanvasElement | null>(null)
let resizeObserver: ResizeObserver | null = null

const drawRuler = () => {
  const canvas = canvasRulerRef.value
  if (!canvas) return
  const ctx = canvas.getContext('2d')
  if (!ctx) return

  const dpr = window.devicePixelRatio || 1
  const rect = canvas.getBoundingClientRect()

  canvas.width = rect.width * dpr
  canvas.height = rect.height * dpr
  ctx.scale(dpr, dpr)

  const width = rect.width
  const height = rect.height

  ctx.clearRect(0, 0, width, height)

  const {
    direction,
    unitSize,
    offset,
    subTickCount,
    valueScale,
    tickColor,
    textColor,
    textOffsetX,
    textOffsetY
  } = props

  const step = unitSize / subTickCount
  const startSub = ((-offset % step) + step) % step
  const firstSubIndex = Math.floor((offset + startSub) / step)

  ctx.strokeStyle = tickColor
  ctx.fillStyle = textColor
  ctx.font = '10px sans-serif'
  ctx.textAlign = 'left'
  ctx.textBaseline = 'top'

  if (direction === 'horizontal') {
    // 小刻度
    for (let i = 0; i <= Math.ceil(width / step); i++) {
      const x = startSub + i * step
      const globalIndex = firstSubIndex + i
      if (globalIndex % subTickCount === 0) continue
      ctx.beginPath()
      ctx.moveTo(x, 0)
      ctx.lineTo(x, height * 0.5)
      ctx.stroke()
    }

    // 主刻度
    const start = ((-offset % unitSize) + unitSize) % unitSize
    const startIndex = Math.floor((offset + start) / unitSize)

    for (let x = start, i = 0; x <= width; x += unitSize, i++) {
      const value = (startIndex + i) * valueScale
      ctx.beginPath()
      ctx.moveTo(x, 0)
      ctx.lineTo(x, height)
      ctx.stroke()
      ctx.fillText(`${value}`, x + textOffsetX, textOffsetY)
    }
  } else if (direction === 'vertical') {
    // 小刻度
    for (let i = 0; i <= Math.ceil(height / step); i++) {
      const y = startSub + i * step
      const globalIndex = firstSubIndex + i
      if (globalIndex % subTickCount === 0) continue
      ctx.beginPath()
      ctx.moveTo(0, y)
      ctx.lineTo(width * 0.5, y)
      ctx.stroke()
    }

    // 主刻度
    const start = ((-offset % unitSize) + unitSize) % unitSize
    const startIndex = Math.floor((offset + start) / unitSize)

    for (let y = start, i = 0; y <= height; y += unitSize, i++) {
      const value = (startIndex + i) * valueScale
      ctx.beginPath()
      ctx.moveTo(0, y)
      ctx.lineTo(width, y)
      ctx.stroke()
      ctx.save()
      ctx.translate(textOffsetX, y + textOffsetY)
      ctx.rotate(-Math.PI / 2) // 逆时针旋转90度(文字朝下)
      ctx.fillText(`${value}`, 0, 0)
      ctx.restore()
    }
  }
}

onMounted(() => {
  nextTick(() => {
    drawRuler()
  })

  resizeObserver = new ResizeObserver(() => {
    drawRuler()
  })

  if (canvasRulerRef.value) {
    resizeObserver.observe(canvasRulerRef.value)
  }
})

onBeforeUnmount(() => {
  if (resizeObserver && canvasRulerRef.value) {
    resizeObserver.unobserve(canvasRulerRef.value)
  }
})

watch(
  () => [
    props.direction,
    props.unitSize,
    props.subTickCount,
    props.offset,
    props.tickColor,
    props.textColor,
    props.valueScale,
    props.textOffsetX,
    props.textOffsetY
  ],
  drawRuler
)
</script>

<style scoped>
.ruler-canvas {
  width: 100%;
  height: 100%;
  display: block;
  background-color: transparent;
  pointer-events: none;
}
</style>

这个组件的核心部分是canvas,至于为什么不用svg,这就要讲到这两个的区别了

1. 性能与适用场景

  • SVG 适合

    • 图标、Logo、图表(如流程图、地图)等需要无损缩放的场景。
    • 交互需求简单的图形(如点击某个图形元素触发事件)。
    • 图形元素数量较少的情况(因为每个元素都是 DOM 节点,过多会影响性能)。
  • Canvas 适合

    • 像素级操作的场景(如照片编辑、滤镜效果)。
    • 动态生成的复杂图形(如游戏画面、数据可视化动画)。
    • 图形元素数量极多(如粒子效果),此时 Canvas 性能更优。

2. 交互与事件

  • SVG :每个图形元素都是独立的 DOM 节点,可直接绑定事件(如 onclick),交互逻辑简单直观。
  • Canvas:没有内置的图形元素事件系统,需通过计算鼠标坐标与绘制区域的关系手动实现交互,复杂度较高。

然后我们再回到我们的标尺组件,标尺组件总共接受以下几个参数

  • direction: { type: String as () => 'horizontal' | 'vertical', default: 'horizontal' }, //方向
  • tickColor: { type: String, default: '#ccc' }, //刻度颜色
  • textColor: { type: String, default: '#999' }, //字体颜色
  • unitSize: { type: Number, default: 100 }, //单位长度,这个意思就是大刻度间距是多少,由于我们的标尺组件是分为大刻度和小刻度,这个参数指的就是大刻度,如图二
  • subTickCount: { type: Number, default: 5 }, //小刻度的份数,是指大刻度之间有几份小刻度
  • offset: { type: Number, default: 0 }, //标尺开始的刻度值
  • valueScale: { type: Number, default: 100 }, //标尺的缩放值
  • textOffsetX: { type: Number, default: 4 }, //文字的X轴偏移量
  • textOffsetY: { type: Number, default: 10 } //文字的Y轴偏移量

图二

我之所以要自定义自己的标尺组件,是因为某些其他的标尺所占据的区域是整个页面,比如说在我的标尺组件中,横向标尺所占领的区域的高度就是20,不会影响到其他组件,如图三

图三

同理,纵向标尺所占据区域的宽度也就是20,如图四

图四

接下来我们看一下在画布中怎么应用

实际应用

1. 首先准备div

html 复制代码
      <!-- 标尺 -->
      <div
        class="rulerHorizontal"
        :style="{
          backgroundColor: themeStyle[theme].backgroundColor3,
          borderBottom: '1px solid ' + themeStyle[theme].borderColor1,
          width: isCanvasRuler ? '100%' : '0px'
        }"
      >
        <Ruler
          :direction="'horizontal'"
          :offset="canvasPosition.x"
          :unit-size="canvasScale * 100"
        />
      </div>
      <div
        class="rulerVertical"
        :style="{
          backgroundColor: themeStyle[theme].backgroundColor3,
          borderRight: '1px solid ' + themeStyle[theme].borderColor1,
          height: isCanvasRuler ? '100%' : '0px'
        }"
      >
        <Ruler
          :direction="'vertical'"
          :offset="canvasPosition.y"
          :unit-size="canvasScale * 100"
          :text-offset-x="10"
          :text-offset-y="-4"
        />
      </div>
      <div
        class="rulerContainer"
        :style="{
          backgroundColor: themeStyle[theme].backgroundColor3,
          borderBottom: '1px solid ' + themeStyle[theme].borderColor1,
          borderRight: '1px solid ' + themeStyle[theme].borderColor1
        }"
        @click="isCanvasRuler = !isCanvasRuler"
      >
        <span>尺</span>
      </div>

还有样式代码

css 复制代码
    .rulerContainer {
      position: absolute;
      top: 0;
      left: 0;
      width: 20px;
      height: 20px;
      font-size: 12px;
      color: #808080;
      line-height: 20px;
      text-align: center;
      user-select: none;
      cursor: pointer;
    }
    .rulerHorizontal {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 20px;
      transition: all 0.5s ease-in-out;
    }
    .rulerVertical {
      position: absolute;
      top: 0;
      left: 0;
      width: 20px;
      height: 100%;
      transition: all 0.5s ease-in-out;
    }

然后就可以直接使用了

2. 绑定缩放和位移

大家可以看到,在我的两个标尺都,都绑定了这两个参数

  • canvasPosition
  • canvasScale

那么当我们的画布进行缩放或移动时,只需要改变这两个参数的数值即可,如下代码所示

ts 复制代码
  //画布-缩放事件
  canvas.on('scale', () => {
    //获取canvas的缩放级别
    const zoom = canvas.zoom()
    bgCanvas.zoom(zoom, { absolute: true })
    const { canvasScale } = storeToRefs(useCanvasParameterStore())
    canvasScale.value = zoom
  })

  //画布-移动事件
  canvas.on('translate', ({ tx, ty }: { tx: number; ty: number }) => {
    const { canvasPosition } = storeToRefs(useCanvasParameterStore())
    canvasPosition.value.x = -tx
    canvasPosition.value.y = -ty
  })

到此我们的标尺组件就讲解完毕了

相关推荐
会豪3 小时前
工业仿真(simulation)--前端(四)--画布编辑(2)
前端
an__ya__3 小时前
Vue数据响应式reactive
前端·javascript·vue.js
苦逼的搬砖工3 小时前
Flutter UI Components:闲来无事,设计整理了这几年来使用的UI组件库
前端·flutter
想买Rolex和Supra的凯美瑞车主3 小时前
Taro + Vite 开发中 fs.allow 配置问题分析与解决
前端
ruanCat3 小时前
使用 vite 的 base 命令行参数来解决项目部署在 github page 的路径问题
前端·github
Codebee3 小时前
使用Qoder 改造前端UI/UE升级改造实践:从传统界面到现代化体验的华丽蜕变
前端·人工智能
叫我詹躲躲3 小时前
开发提速?Vue3模板隐藏技巧来了
前端·vue.js·ai编程
华仔啊3 小时前
面试都被问懵了?CSS 的 flex:1 和 flex:auto 真不是一回事!90%的人都搞错了
前端·javascript
前端康师傅3 小时前
JavaScript 函数详解
前端·javascript