123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- <template>
- <div
- class="base-radar-view"
- :style="{ width: canvasSize.width + 'px', height: canvasSize.height + 'px' }"
- >
- <canvas ref="canvasRef" :width="canvasSize.width" :height="canvasSize.height"></canvas>
- <div class="overlay-layer">
- <div
- v-for="target in targets"
- :key="target.id"
- class="target-point"
- :style="getTargetStyle(target)"
- >
- <span class="target-id">{{ target.id }}</span>
- </div>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { onMounted, ref, watch, type CSSProperties } from 'vue'
- import radarUrl from '@/assets/furnitures/radar.png'
- import { convert_region_r2c, rotateRect } from '@/utils/coordTransform'
- import type { TargetPoint } from '@/types/radar'
- defineOptions({ name: 'BaseRadarView' })
- interface CanvasSize {
- width: number
- height: number
- }
- interface Props {
- coordinates: number[] // 检测区域坐标
- angle?: number // 旋转角度,默认 0 度
- targets?: TargetPoint[] // 目标点数组
- canvasSize?: CanvasSize // 画布尺寸,默认 500x500
- mode?: 'view' | 'edit'
- }
- const props = withDefaults(defineProps<Props>(), {
- canvasSize: () => ({
- width: 500,
- height: 500,
- }),
- angle: 0,
- mode: 'view',
- })
- const canvasRef = ref<HTMLCanvasElement | null>(null)
- let ctx: CanvasRenderingContext2D | null = null
- /**
- * 网格选项
- * 用于绘制背景网格的选项
- * @param width 画布宽度
- * @param height 画布高度
- * @param spacing 网格间距,默认 50
- * @param color 网格线颜色,默认 '#d9d9d7'
- * @param lineWidth 网格线宽度,默认 0.5
- */
- interface GridOptions {
- width: number
- height: number
- spacing?: number
- color?: string
- lineWidth?: number
- }
- /**
- * 绘制背景网格
- * 50px间距的灰色网格线,辅助定位
- * @param ctx 画布上下文
- * @param options 网格选项
- */
- const drawGrid = (ctx: CanvasRenderingContext2D, options: GridOptions) => {
- const { width, height, spacing = 50, color = '#d9d9d7', lineWidth = 0.5 } = options
- if (!ctx) return
- ctx.strokeStyle = color
- ctx.lineWidth = lineWidth
- // 水平网格线
- for (let y = spacing; y < height; y += spacing) {
- ctx.beginPath()
- ctx.moveTo(0, y)
- ctx.lineTo(width, y)
- ctx.stroke()
- }
- // 垂直网格线
- for (let x = spacing; x < width; x += spacing) {
- ctx.beginPath()
- ctx.moveTo(x, 0)
- ctx.lineTo(x, height)
- ctx.stroke()
- }
- }
- /**
- * 坐标轴选项
- * 用于绘制坐标轴的选项
- * @param width 画布宽度
- * @param height 画布高度
- * @param angle 旋转角度(单位:度),默认 0
- * @param xColor X轴颜色,默认 'red'
- * @param yColor Y轴颜色,默认 'green'
- */
- interface CoordinateOptions {
- width: number
- height: number
- angle?: number
- xColor?: string
- yColor?: string
- }
- /**
- * 绘制坐标轴
- * 虚线样式的X轴和Y轴,标识坐标系中心
- * @param ctx 画布上下文
- * @param options 坐标轴选项
- */
- const drawCoordinateAxes = (ctx: CanvasRenderingContext2D, options: CoordinateOptions) => {
- const { width, height, angle = 0, xColor = '#2c3e50', yColor = '#2c3e50' } = options
- if (!ctx) return
- const centerX = width / 2
- const centerY = height / 2
- // const arrowSize = 10
- // 只接受 0, 90, 180, 270,其他归为 0
- const validAngles: number[] = [0, 90, 180, 270]
- const rotationDeg: number = validAngles.includes(angle) ? angle : 0
- const rotationRad = (rotationDeg * Math.PI) / 180
- ctx.save()
- ctx.translate(centerX, centerY)
- ctx.rotate(rotationRad)
- ctx.translate(-centerX, -centerY)
- ctx.lineWidth = 1
- ctx.setLineDash([5, 5])
- // 绘制X轴(水平线)
- ctx.strokeStyle = xColor
- ctx.beginPath()
- ctx.moveTo(0, centerY)
- ctx.lineTo(width, centerY)
- ctx.stroke()
- // X轴箭头(右端)
- // ctx.beginPath()
- // ctx.moveTo(width, centerY)
- // ctx.lineTo(width - arrowSize, centerY - arrowSize / 2)
- // ctx.lineTo(width - arrowSize, centerY + arrowSize / 2)
- // ctx.closePath()
- // ctx.fillStyle = xColor
- // ctx.fill()
- // 绘制Y轴(垂直线)
- ctx.strokeStyle = yColor
- ctx.beginPath()
- ctx.moveTo(centerX, 0)
- ctx.lineTo(centerX, height)
- ctx.stroke()
- // Y轴箭头(顶部)
- // ctx.beginPath()
- // ctx.moveTo(centerX, 0)
- // ctx.lineTo(centerX - arrowSize / 2, arrowSize)
- // ctx.lineTo(centerX + arrowSize / 2, arrowSize)
- // ctx.closePath()
- // ctx.fillStyle = yColor
- // ctx.fill()
- ctx.setLineDash([])
- ctx.restore()
- }
- /**
- * 雷达图图片选项
- * 用于绘制雷达图图片的选项
- * @param width 画布宽度
- * @param height 画布高度
- * @param radarImage 雷达图图片
- * @param angle 旋转角度,默认 0
- * @param size 图片尺寸(宽高),默认 20
- * @param offset 图片偏移角度,默认 90 度 指向X轴
- */
- interface RadarImageOptions {
- width: number
- height: number
- radarImage: HTMLImageElement
- angle?: number
- size?: number
- offset?: number
- }
- /**
- * 绘制雷达图
- * 图片以指定角度旋转,默认指向X轴
- * @param ctx 画布上下文
- * @param options 雷达图图片选项
- */
- const drawRadarImage = (ctx: CanvasRenderingContext2D, options: RadarImageOptions) => {
- const { width, height, angle = 0, size = 20, radarImage, offset = 90 } = options
- const centerX = width / 2
- const centerY = height / 2
- const halfSize = size / 2
- const angleRad = ((angle + offset) * Math.PI) / 180
- ctx.save()
- ctx.translate(centerX, centerY)
- ctx.rotate(angleRad)
- ctx.drawImage(radarImage, -halfSize, -halfSize, size, size)
- ctx.restore()
- }
- /**
- * 检测区域选项
- * 用于绘制检测区域的选项
- * @param area 检测区域坐标 [xStart, xEnd, yStart, yEnd]
- * @param radarPosition 雷达图位置 { x_radar, y_radar }
- * @param angle 旋转角度(单位:度),默认 0
- */
- interface DetectionAreaOptions {
- area: number[] // 检测区域坐标 [xStart, xEnd, yStart, yEnd]
- radarPosition: {
- x_radar: number
- y_radar: number
- }
- angle?: number
- }
- /**
- * 绘制检测区域
- * @param ctx 画布上下文
- * @param options 检测区域选项
- */
- const drawDetectionArea = (ctx: CanvasRenderingContext2D, options: DetectionAreaOptions) => {
- const { area, radarPosition, angle = 0 } = options
- const [xStart, xEnd, yStart, yEnd] = area
- const canvasRect = convert_region_r2c(
- {
- x_cm_start: xStart,
- x_cm_stop: xEnd,
- y_cm_start: yStart,
- y_cm_stop: yEnd,
- },
- radarPosition
- )
- const rotatedRect = rotateRect(
- {
- left: canvasRect.left,
- top: canvasRect.top,
- width: canvasRect.width,
- height: canvasRect.height,
- },
- { x: radarPosition.x_radar, y: radarPosition.y_radar },
- angle
- )
- if (!ctx) return
- // 绘制蓝色边框
- ctx.strokeStyle = '#1677ff'
- ctx.lineWidth = 1
- ctx.strokeRect(rotatedRect.left, rotatedRect.top, rotatedRect.width, rotatedRect.height)
- // 添加半透明蓝色填充
- ctx.fillStyle = 'rgba(52, 152, 219, 0.3)'
- ctx.fillRect(rotatedRect.left, rotatedRect.top, rotatedRect.width, rotatedRect.height)
- }
- /**
- * 绘制画布
- */
- const drawCanvans = () => {
- /* 初始化画布 */
- if (!canvasRef.value) return
- ctx = canvasRef.value.getContext('2d')
- if (!ctx) return
- const CANVAS_W = props.canvasSize.width
- const CANVAS_H = props.canvasSize.height
- const CANVAS_SIZE = {
- width: CANVAS_W,
- height: CANVAS_H,
- }
- const radarPosition = {
- x_radar: CANVAS_W / 2,
- y_radar: CANVAS_H / 2,
- }
- /* 清除画布并设置背景色 */
- ctx.clearRect(0, 0, CANVAS_W, CANVAS_H)
- ctx.fillStyle = '#f2f2f0'
- ctx.fillRect(0, 0, CANVAS_W, CANVAS_H)
- /* 绘制网格和坐标轴 */
- drawGrid(ctx, { width: CANVAS_W, height: CANVAS_H })
- drawCoordinateAxes(ctx, { ...CANVAS_SIZE, angle: props.angle })
- /* 绘制雷达图 */
- const radarImage = new Image()
- radarImage.src = radarUrl
- radarImage.onload = () => {
- if (!ctx) return
- drawRadarImage(ctx, { ...CANVAS_SIZE, radarImage, angle: props.angle })
- }
- /* 绘制检测区域 */
- drawDetectionArea(ctx, { area: props.coordinates, radarPosition, angle: props.angle })
- }
- // 监听角度变化,重新绘制画布
- watch(
- () => [props.coordinates, props.angle],
- () => {
- drawCanvans()
- },
- {
- immediate: true,
- }
- )
- onMounted(() => {
- drawCanvans()
- })
- /**
- * 获取目标点样式
- * @param target 目标点
- * @returns 目标点样式
- */
- const getTargetStyle = (target: TargetPoint) => {
- const colorPalette = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple']
- const colorIndex = target.id < colorPalette.length ? target.id : colorPalette.length - 1
- const color = colorPalette[colorIndex]
- return {
- position: 'absolute',
- width: '18px',
- height: '18px',
- background: color,
- borderRadius: '50%',
- transform: `translate3d(${target.displayX}px, ${target.displayY}px, 0) translate(-50%, -50%)`,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- color: 'white',
- fontSize: '12px',
- fontWeight: '600',
- pointerEvents: 'none',
- zIndex: 10,
- transition: 'transform 1s linear',
- willChange: 'transform',
- boxShadow: '0 0 8px rgba(0, 0, 0, 0.5)',
- userSelect: 'none',
- } as CSSProperties
- }
- </script>
- <style scoped lang="less">
- .base-radar-view {
- position: relative;
- display: flex;
- canvas {
- border: 2px solid #ddd;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- pointer-events: none;
- }
- .overlay-layer {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- pointer-events: auto;
- .target-point {
- position: absolute;
- pointer-events: none;
- }
- }
- }
- </style>
|