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.

294 lines
6.6 KiB
Vue

5 months ago
<template>
<!-- <div class="container" ref="containerRef" @wheel.prevent="onWheel">-->
<div class="container" ref="containerRef">
<!-- 左侧竖向刻度尺 -->
<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
class="box"
:style="{
width: boxSize + 'px',
height: boxSize + 'px',
left: boxPos.x + 'px',
top: contentHeight - boxPos.y - boxSize + 'px',
}"
></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} from 'vue'
const props = defineProps({
boxPos: {
type: Object,
default: () => ({x: 200, y: 300}),
},
})
const boxPos = ref({x: 200, y: 300})
// setInterval(() => {
// boxPos.value.x += (-10 + (Math.random() * 20))
// boxPos.value.y += (-10 + (Math.random() * 20))
// }, 100)
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 = boxPos.value.x * s + (boxSize * s) / 2
const y = (contentHeight.value - boxPos.value.y - boxSize / 2) * s
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
const oldCenter = getBoxCenterPos(oldScale)
const newCenter = getBoxCenterPos(newScale)
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-color: #00bfff;
border-radius: 3px;
}
.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;
}
</style>