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
294 lines
6.6 KiB
Vue
<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>
|