You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

428 lines
9.6 KiB
Vue

3 months ago
<template>
<!-- <div class="container" ref="containerRef" @wheel.prevent="onWheel">-->
<div class="container" ref="containerRef">
<!-- <div class="leftLine"></div>-->
<!-- 左侧竖向刻度尺 -->
<div class="ruler vertical" :style="{ backgroundColor: bgColor }">
<div class="ruler-content" :style="{ height: contentHeight + 'px' ,transform: 'rotate(180deg)'}">
<template v-for="tick in verticalTicks" :key="'v-' + tick.index">
<div
class="tick"
:class="{ major: tick.isMajor }"
:style="{
bottom: tick.pos + 'px',
width: tick.isMajor ? '15px' : '7px',
right: 0,
}"
>
<span
v-if="tick.isMajor"
class="label"
:style="{ right: '18px', bottom: '-6px',transform: 'rotate(180deg)' }"
>
{{ tick.label }}
</span>
</div>
</template>
</div>
</div>
<!-- 内容区 -->
<div
class="content-area"
:style="{
width: contentWidth + 'px',
height: contentHeight + 'px',
backgroundColor: bgColor,
}"
>
<div
class="content-inner"
:style="{
transform: `translate(${offsetX}px, ${offsetY}px) scale(${scale})`,
transformOrigin: 'top left',
}"
>
<div
v-if="props.boxPos.x && props.boxPos.y"
class="box"
:style="{
width: boxSize + 'px',
height: boxSize + 'px',
left: props.boxPos.x + 'px',
top: contentHeight - props.boxPos.y - boxSize + 'px',
transform: `rotate(${props.boxPos.rotate}deg)`
}"
>
<div class="scan"></div>
</div>
<div class="dot"
v-for="i in dots"
:style="{
width: 10 + 'px',
height: 10 + 'px',
left: i.x + 'px',
top: contentHeight - i.y - 10 + 'px',
}">
</div>
<div
v-if="lineStyle"
class="line"
:style="lineStyle"
></div>
</div>
</div>
<!-- 下方横向刻度尺 -->
<div
class="ruler horizontal"
:style="{ backgroundColor: bgColor, width: contentWidth + 'px' }"
>
<div class="ruler-content" :style="{ width: contentWidth + 'px' }">
<template v-for="tick in horizontalTicks" :key="'h-' + tick.index">
<div
class="tick"
:class="{ major: tick.isMajor }"
:style="{
left: tick.pos + 'px',
height: tick.isMajor ? '15px' : '7px',
top: 0,
}"
>
<span
v-if="tick.isMajor"
class="label"
:style="{ top: '18px', left: '-6px' }"
>
{{ tick.label }}
</span>
</div>
</template>
</div>
</div>
</div>
</template>
<script setup>
import {ref, computed, onMounted, watch, watchEffect} from 'vue'
const props = defineProps({
boxPos: {
type: Object,
default: () => ({x: 200, y: 300, rotate: -45}),
},
index: {type: Number, default: -1} // 默认不选
})
// const boxPos = ref({x: 200, y: 300, rotate: -45})
// setInterval(() => {
// props.boxPos.x += (-10 + (Math.random() * 20))
// props.boxPos.y += (-10 + (Math.random() * 20))
// props.boxPos.rotate += (-5 + (Math.random() * 10))
// }, 1000)
const dots = ref([
// {
// x: 100,
// y: 100,
// },
// {
// x: 200,
// y: 200,
// }
])
const setDot = (dot, bol) => {
if (bol) {
dots.value = []
} else {
dots.value = [...dots.value, ...dot]
}
}
const lineStyle = ref(null)
watchEffect(() => {
if (props.index < 0 || !dots.value[props.index]) return
const dot = dots.value[props.index]
// box 中心点
const centerX = props.boxPos.x + boxSize / 2
const centerY = contentHeight.value - props.boxPos.y - boxSize / 2
// box 旋转角度
const rad = props.boxPos.rotate * Math.PI / 180
// 未旋转时顶部中心点(相对中心点)
const dx0 = 0
const dy0 = -boxSize / 2
// 旋转后的偏移
const dx = dx0 * Math.cos(rad) - dy0 * Math.sin(rad)
const dy = dx0 * Math.sin(rad) + dy0 * Math.cos(rad)
// 顶部中心点
const boxTopX = centerX + dx
const boxTopY = centerY + dy
// dot 中心点
const dotX = dot.x + 5
const dotY = contentHeight.value - dot.y - 5
const dX = dotX - boxTopX
const dY = dotY - boxTopY
const length = Math.sqrt(dX * dX + dY * dY)
const angle = Math.atan2(dY, dX) * 180 / Math.PI
// 先清掉上一次的定时器,避免抖动(可选)
// if (lineStyle._timer) clearTimeout(lineStyle._timer)
// 延迟 1 秒更新样式
lineStyle._timer = setTimeout(() => {
lineStyle.value = {
left: boxTopX + 'px',
top: boxTopY + 'px',
width: length + 'px',
transform: `rotate(${angle}deg)`,
transformOrigin: '0 0'
}
}, 1000)
})
defineExpose({
setDot
})
const boxSize = 30
const minScale = 1
const maxScale = 3
const scale = ref(1)
const offsetX = ref(0)
const offsetY = ref(0)
const containerRef = ref(null)
const rulerSize = 40
const containerWidth = ref(0)
const containerHeight = ref(0)
const contentWidth = computed(() => containerWidth.value - rulerSize)
const contentHeight = computed(() => containerHeight.value - rulerSize)
const bgColor = '#353741'
function updateContainerSize() {
if (containerRef.value) {
containerWidth.value = containerRef.value.clientWidth
containerHeight.value = containerRef.value.clientHeight
}
}
onMounted(() => {
updateContainerSize()
window.addEventListener('resize', updateContainerSize)
})
watch(
() => containerRef.value,
(el) => {
if (el) updateContainerSize()
}
)
// 获取缩放后 box 的中心坐标(左下角为原点)
function getBoxCenterPos(s) {
const x = props.boxPos.x * s + (boxSize * s) / 2
const y = props.boxPos.y * s + (boxSize * s) / 2
return {x, y}
}
// 缩放逻辑
function onWheel(e) {
const oldScale = scale.value
const direction = e.deltaY < 0 ? 1 : -1
const step = 0.1
let newScale = oldScale + direction * step
newScale = Math.max(minScale, Math.min(maxScale, newScale))
if (newScale === oldScale) return
// box 中心点在旧比例下的位置
const oldCenter = getBoxCenterPos(oldScale)
// box 中心点在新比例下的位置
const newCenter = getBoxCenterPos(newScale)
// 保持 box 中心点在画布中的位置不变
offsetX.value += oldCenter.x - newCenter.x
offsetY.value += oldCenter.y - newCenter.y
scale.value = newScale
}
// 刻度计算
const baseMajorTickSize = 100
const baseMinorTicks = 10
const majorTickSpacing = computed(() => baseMajorTickSize * scale.value)
const minorTickSpacing = computed(() => majorTickSpacing.value / baseMinorTicks)
function generateTicks(length, offset, isVertical = false) {
const spacing = minorTickSpacing.value
const ticks = []
const startIndex = Math.floor(-offset / spacing)
const endIndex = Math.ceil((length - offset) / spacing)
for (let i = startIndex; i <= endIndex; i++) {
const rawPos = i * spacing + offset
const pos = isVertical ? length - rawPos : rawPos
const isMajor = i % baseMinorTicks === 0
ticks.push({
index: i,
pos,
isMajor,
label: isMajor ? i * (baseMajorTickSize / baseMinorTicks) : null,
})
}
return ticks
}
const horizontalTicks = computed(() =>
generateTicks(contentWidth.value, offsetX.value)
)
const verticalTicks = computed(() =>
generateTicks(contentHeight.value, offsetY.value, true)
)
</script>
<style scoped>
.container {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
background: #353741;
user-select: none;
overflow: hidden;
color: white;
}
/* 竖向刻度尺 */
.ruler.vertical {
width: 40px;
height: calc(100% - 40px);
position: relative;
border-right: 1px solid #666;
}
.ruler.horizontal {
height: 40px;
width: 100%;
position: absolute;
bottom: 0;
left: 40px;
border-top: 1px solid #666;
}
.content-area {
position: relative;
height: 100%;
overflow: hidden;
flex-shrink: 0;
border-left: 1px solid #666;
border-bottom: 1px solid #666;
}
.content-inner {
position: absolute;
width: 100%;
height: 100%;
}
.box {
position: absolute;
background-image: url("../assets/car.png");
background-repeat: no-repeat;
background-size: 100% 100%;
transition: top 1s ease, left 1s ease;
}
.box .scan {
position: absolute;
background-image: url("../assets/scan.gif");
background-size: 100% 100%;
width: 100px;
height: 100px;
top: 50%;
right: 15px;
transform: translateY(-50%);
}
.dot {
position: absolute;
background: radial-gradient(circle at center,
#ea1212 0%, rgba(228, 116, 116, 0.2) 100%);
border-radius: 50%;
}
.tick {
position: absolute;
opacity: 0.4;
pointer-events: none;
}
.tick.major {
opacity: 1;
}
.ruler.vertical .tick {
border-top: 1px solid white;
}
.ruler.vertical .tick.major {
border-top: 2px solid white;
}
.ruler.vertical .label {
position: absolute;
font-size: 10px;
bottom: -6px;
right: 18px;
}
.ruler.horizontal .tick {
border-left: 1px solid white;
}
.ruler.horizontal .tick.major {
border-left: 2px solid white;
}
.ruler.horizontal .label {
position: absolute;
font-size: 10px;
top: 18px;
left: -6px;
}
.ruler-content {
position: relative;
}
.leftLine {
position: absolute;
top: 0;
left: 20%;
width: 1px;
height: calc(100% - 40px);
border-right: 3px dashed white;
z-index: 9999;
}
.line {
position: absolute;
height: 2px;
background: yellow;
}
</style>