项目中遇到了个类似LOL小地图的需求,电子放大
没找到合适的 DEMO,自己写了个(基于vue3 + ts) ,以供参考
实现功能:
1. 在主视框中滚动缩放内容,右小角小视窗红框选出对应区域
2. 小视窗滚动可以缩放主视框
3. 主视框可以拖动内容,右小角小视窗红框同步移动
4. 拖动右小角小视窗红框,可调整主视框放大内容
效果图:
上代码:
<script lang="ts" setup>
import { ref, reactive, onMounted, computed, Ref } from "vue"
interface Position {
x: number
y: number
}
const rollBoxContainer: Ref<HTMLElement | null> = ref(null)
const zoomElement: Ref<HTMLElement | null> = ref(null)
const smallWindow: Ref<HTMLElement | null> = ref(null)
const redBox: Ref<HTMLElement | null> = ref(null)
const zoomElementValue = computed(() => zoomElement.value)
const rollBoxContainerValue = computed(() => rollBoxContainer.value)
const mainCurScale = ref(1)
const mousePosition: Position = reactive({ x: 0, y: 0 })
const calcMousePosition: Position = reactive({ x: 0, y: 0 })
const mainRollingStyle = computed(() => ({
transformOrigin: `${mousePosition.x}px ${mousePosition.y}px`,
transform: `scale(${mainCurScale.value})`,
cursor: mainCurScale.value > 1 ? "grab" : ""
}))
const redBoxStyle = computed(() => ({
transformOrigin: `${calcMousePosition.x}px ${calcMousePosition.y}px`,
transform: `scale(${1 / mainCurScale.value})`,
border: `${mainCurScale.value * 2}px solid #ff6969`
}))
/**
* 工具函数
* 1. 获取元素位置信息
* 2. 检查移动范围
* 3. 修正移动位置
*/
function getRect(el: Ref<HTMLElement | null>): DOMRect {
return el.value?.getBoundingClientRect()!
}
function checkSafeMove(zoomElement: Ref<HTMLElement | null>, rollBoxContainer: Ref<HTMLElement | null>): boolean {
if (zoomElement.value) {
const { transformOrigin } = getComputedStyle(zoomElement.value)
const { height, width } = getRect(rollBoxContainer)
const [originX, originY] = transformOrigin.split(" ").map((i) => parseFloat(i))
const safeTopLeft = originX >= 0 && originY >= 0
const safeBottomRight = originX <= width && originY <= height
return safeTopLeft && safeBottomRight
}
return false
}
function fixPosition(position: Position, container: Ref<HTMLElement | null>) {
const { height, width } = getRect(container)
const { x, y } = position
position.x = Math.min(Math.max(x, 0), width)
position.y = Math.min(Math.max(y, 0), height)
}
// 滚动大图
function rollingMainWindow(e: WheelEvent) {
const { clientX, clientY } = e
const delta = Math.sign(e.deltaY)
mainCurScale.value -= delta * 0.1
mainCurScale.value = Math.max(mainCurScale.value, 1)
const rollBoxRect = getRect(rollBoxContainer)
const { left, top, width, height } = getRect(zoomElement)
mousePosition.x = ((clientX - left) / width) * rollBoxRect.width
mousePosition.y = ((clientY - top) / height) * rollBoxRect.height
synchronizeMoveRedBox()
}
function synchronizeMoveRedBox() {
const rollBoxRect = getRect(rollBoxContainer)
const { width, height } = getRect(smallWindow)
calcMousePosition.x = (mousePosition.x / rollBoxRect.width) * width
calcMousePosition.y = (mousePosition.y / rollBoxRect.height) * height
}
// 处理拖拽大图
const startMove = ref(false)
function handleStartMove() {
startMove.value = true
}
function handleMovingZoomElement(e: MouseEvent) {
if (!startMove.value) {
return
}
if (!checkSafeMove(zoomElement, rollBoxContainer)) {
fixPosition(mousePosition, rollBoxContainer)
return
}
const scale = mainCurScale.value
const { movementX, movementY } = e
mousePosition.x -= movementX / scale
mousePosition.y -= movementY / scale
synchronizeMoveRedBox()
}
// 小窗口滚动
function rollingSmallWindow(e: WheelEvent) {
const { clientX, clientY } = e
const delta = Math.sign(e.deltaY)
mainCurScale.value += delta * 0.1
mainCurScale.value = Math.max(mainCurScale.value, 1)
}
// 处理拖动小窗口
const startMoveRedBox = ref(false)
function handleRedBoxStartMove(e: MouseEvent) {
startMoveRedBox.value = true
}
function handleRedBoxSMoving(e: MouseEvent) {
if (!startMoveRedBox.value) {
return
}
if (!checkSafeMove(redBox, smallWindow)) {
fixPosition(calcMousePosition, smallWindow)
return
}
const { movementX, movementY } = e
const scale = mainCurScale.value
calcMousePosition.x += movementX * scale
calcMousePosition.y += movementY * scale
synchronizeMoveZoomElement()
}
function synchronizeMoveZoomElement() {
if (rollBoxContainer.value && smallWindow.value) {
const rollBoxRect = getRect(rollBoxContainer)
const { width, height } = getRect(smallWindow)
mousePosition.x = (calcMousePosition.x / width) * rollBoxRect.width
mousePosition.y = (calcMousePosition.y / height) * rollBoxRect.height
}
}
onMounted(() => {
window.addEventListener("mouseup", () => {
startMove.value = false
startMoveRedBox.value = false
})
})
</script>
<style lang='scss' scoped>
@mixin fill {
height: 100%;
width: 100%;
}
.app-container {
user-select: none;
-webkit-user-drag: none;
.main-container {
height: 500px;
max-width: 900px;
position: relative;
border: 1px solid #fff;
overflow: hidden;
.zoom-element {
@include fill();
}
.small-window {
height: 30%;
width: 30%;
position: absolute;
right: -1px;
bottom: -1px;
border: 1px solid #fff;
.red-box-container {
@include fill();
background-image: url(http://surl.li/dpeat);
background-size: 100%;
.red-box {
@include fill();
cursor: grab;
}
}
}
}
}
</style>
<template>
<div class="app-container">
<div
ref="rollBoxContainer"
class="main-container"
@mousewheel.prevent="rollingMainWindow"
@mousedown="handleStartMove"
@mousemove="handleMovingZoomElement"
>
<img
src="http://surl.li/dpeat"
ref="zoomElement"
:style="mainRollingStyle"
class="zoom-element"
:draggable="false"
/>
<div ref="smallWindow" class="small-window" @mousedown.stop @mousemove.stop @mousewheel.stop="rollingSmallWindow">
<div class="red-box-container">
<div
ref="redBox"
v-show="mainCurScale > 1"
:style="redBoxStyle"
class="red-box"
@mousedown.stop="handleRedBoxStartMove"
@mousemove.stop="handleRedBoxSMoving"
/>
</div>
</div>
</div>
</div>
</template>