forked from Ivasoft/geovisio-website
302 lines
7.8 KiB
Vue
302 lines
7.8 KiB
Vue
<template>
|
|
<div class="wrapper-widget-orientation">
|
|
<div
|
|
class="wrapper-img-road"
|
|
:style="{
|
|
transform: `rotate(${roadDegrees}deg)`
|
|
}"
|
|
></div>
|
|
<div class="wrapper-widget">
|
|
<div class="wrapper-car">
|
|
<div class="wrapper-elements">
|
|
<div class="rounded-transparent"></div>
|
|
<img
|
|
src="@/assets/images/car.svg"
|
|
alt=""
|
|
:style="{
|
|
transform: `rotate(${roadDegrees}deg)`
|
|
}"
|
|
class="car-img"
|
|
/>
|
|
<div
|
|
:style="{
|
|
transform: `rotate(${angle}deg)`
|
|
}"
|
|
id="rotateWrapper"
|
|
class="rotate-wrapper"
|
|
>
|
|
<div
|
|
id="rotate"
|
|
@mousedown="mousedown"
|
|
@mousemove="handleMouseMove"
|
|
@mouseup="mouseup"
|
|
@mouseout="mouseup"
|
|
class="cursor-img desktop"
|
|
>
|
|
<button class="arrow-img arrow-img-1" @click="clickAndMove(45)">
|
|
<img src="@/assets/images/icon/cursor-arrow.svg" alt="" />
|
|
</button>
|
|
<button class="arrow-img arrow-img-2" @click="clickAndMove(-45)">
|
|
<img src="@/assets/images/icon/cursor-arrow.svg" alt="" />
|
|
</button>
|
|
</div>
|
|
<div
|
|
id="rotate"
|
|
@touchstart="touchdown"
|
|
@touchmove="handleTouchMove"
|
|
@touchend="mouseup"
|
|
class="cursor-img responsive"
|
|
>
|
|
<button class="arrow-img arrow-img-1" @click="clickAndMove(45)">
|
|
<img src="@/assets/images/icon/cursor-arrow.svg" alt="" />
|
|
</button>
|
|
<button class="arrow-img arrow-img-2" @click="clickAndMove(-45)">
|
|
<img src="@/assets/images/icon/cursor-arrow.svg" alt="" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref, watchEffect } from 'vue'
|
|
import { modulo180 } from '@/utils/calc'
|
|
let angleValue = ref<number>(0)
|
|
let angle = ref<number>(0)
|
|
let prevRotation = ref<number>(0)
|
|
let active = ref<boolean>(false)
|
|
let rotation = ref<number>(0)
|
|
let startAngle = ref<number>(0)
|
|
let rotateWrapper = ref<HTMLElement | null>(null)
|
|
let rotate = ref<HTMLElement | null>(null)
|
|
let center: { x: number; y: number } = { x: 0, y: 0 }
|
|
const R2D: number = 180 / Math.PI
|
|
const emit = defineEmits<{
|
|
(e: 'triggerAngle', value: number): void
|
|
(e: 'triggerMovingAngle', value: number): void
|
|
}>()
|
|
const props = defineProps({
|
|
roadDegrees: { type: Number, default: 0 },
|
|
seqBruteDeg: { type: Number, default: 0 }
|
|
})
|
|
watchEffect(() => {
|
|
angle.value = Math.round(props.seqBruteDeg)
|
|
})
|
|
onMounted(() => {
|
|
const app = document.getElementById('app')
|
|
rotateWrapper.value = document.getElementById('rotateWrapper')
|
|
rotate.value = document.getElementById('rotate')
|
|
if (app) app.addEventListener('mouseup', () => (active.value = false))
|
|
})
|
|
function touchdown(e: TouchEvent): void {
|
|
e.preventDefault()
|
|
if (!rotateWrapper.value) return
|
|
const bb = rotateWrapper.value.getBoundingClientRect()
|
|
const { top: t, left: l, height: h, width: w } = bb
|
|
center = { x: l + w / 2, y: t + h / 2 }
|
|
let x = e.changedTouches[0].clientX - center.x
|
|
let y = e.changedTouches[0].clientY - center.y
|
|
startAngle.value = R2D * Math.atan2(y, x)
|
|
active.value = true
|
|
}
|
|
function mousedown(e: MouseEvent): void {
|
|
e.preventDefault()
|
|
if (!rotateWrapper.value) return
|
|
const bb = rotateWrapper.value.getBoundingClientRect()
|
|
const { top: t, left: l, height: h, width: w } = bb
|
|
center = { x: l + w / 2, y: t + h / 2 }
|
|
let x = e.clientX - center.x
|
|
let y = e.clientY - center.y
|
|
startAngle.value = R2D * Math.atan2(y, x)
|
|
active.value = true
|
|
}
|
|
function handleTouchMove(e: TouchEvent): void {
|
|
if (!active.value || !rotateWrapper.value) return
|
|
const x = e.changedTouches[0].clientX - center.x
|
|
const y = e.changedTouches[0].clientY - center.y
|
|
const d = R2D * Math.atan2(y, x)
|
|
rotation.value = d - startAngle.value
|
|
const calc = angle.value + rotation.value
|
|
rotateWrapper.value.style.transform = `rotate(${calc}deg)`
|
|
}
|
|
function handleMouseMove(e: MouseEvent): void {
|
|
if (!active.value || !rotateWrapper.value) return
|
|
const x = e.clientX - center.x
|
|
const y = e.clientY - center.y
|
|
const d = R2D * Math.atan2(y, x)
|
|
rotation.value = d - startAngle.value
|
|
const calc = angle.value + rotation.value
|
|
rotateWrapper.value.style.transform = `rotate(${calc}deg)`
|
|
}
|
|
function mouseup(): void {
|
|
if (!active.value || !rotate.value) return
|
|
angle.value += Math.round(rotation.value)
|
|
if (rotation.value !== prevRotation.value) {
|
|
prevRotation.value = rotation.value
|
|
angleValue.value = angle.value
|
|
if (angleValue.value !== 0) {
|
|
emit('triggerMovingAngle', modulo180(angle.value, props.roadDegrees))
|
|
emit('triggerAngle', modulo180(angle.value, props.roadDegrees))
|
|
}
|
|
}
|
|
active.value = false
|
|
}
|
|
function clickAndMove(value: number): void {
|
|
const moduloAngle = modulo180(angle.value, Math.round(props.roadDegrees))
|
|
if (moduloAngle % 45 === 0) {
|
|
let angleToEmit = moduloAngle + value
|
|
if (angleToEmit > 180) angleToEmit = -135
|
|
if (angleToEmit < -180) angleToEmit = 135
|
|
return emit('triggerAngle', angleToEmit)
|
|
}
|
|
let closestMultiple = Math.ceil(moduloAngle / 45) * value
|
|
return emit('triggerAngle', closestMultiple)
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.wrapper-widget-orientation {
|
|
position: relative;
|
|
overflow: hidden;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: toRem(40);
|
|
width: 100%;
|
|
border-radius: toRem(1);
|
|
}
|
|
.wrapper-img-road {
|
|
background-image: url('@/assets/images/road.svg');
|
|
background-repeat: no-repeat;
|
|
background-size: contain;
|
|
background-position: center;
|
|
height: 210%;
|
|
width: 250%;
|
|
}
|
|
.wrapper-widget {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
.wrapper-car {
|
|
height: toRem(40);
|
|
width: toRem(40);
|
|
display: flex;
|
|
justify-content: center;
|
|
position: relative;
|
|
}
|
|
|
|
.wrapper-elements {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.rotate-wrapper {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
transform: rotate(0);
|
|
background-color: transparent;
|
|
display: flex;
|
|
justify-content: center;
|
|
position: relative;
|
|
z-index: 0;
|
|
.cursor-img {
|
|
background-image: url('@/assets/images/cursor.svg');
|
|
content: '';
|
|
display: block;
|
|
position: absolute;
|
|
left: toRem(2);
|
|
right: 0;
|
|
top: 0;
|
|
margin: auto;
|
|
height: 50%;
|
|
width: 74%;
|
|
background-repeat: no-repeat;
|
|
background-size: cover;
|
|
background-position: center;
|
|
cursor: move;
|
|
}
|
|
.desktop {
|
|
display: block;
|
|
}
|
|
.responsive {
|
|
display: none;
|
|
}
|
|
}
|
|
.arrow-img {
|
|
position: absolute;
|
|
border: none;
|
|
background-color: transparent;
|
|
height: toRem(5);
|
|
width: toRem(5);
|
|
padding: 0;
|
|
img {
|
|
height: 100%;
|
|
&:hover {
|
|
opacity: 0.8;
|
|
}
|
|
}
|
|
}
|
|
.arrow-img-1 {
|
|
right: toRem(4);
|
|
top: toRem(6);
|
|
}
|
|
.arrow-img-2 {
|
|
left: toRem(3.2);
|
|
bottom: toRem(10.5);
|
|
transform: rotate(90deg);
|
|
}
|
|
.car-img {
|
|
position: absolute;
|
|
height: 45%;
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
}
|
|
.rounded-transparent {
|
|
background-color: rgba(white, 0.4);
|
|
height: toRem(25);
|
|
width: toRem(25);
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
@media (max-width: toRem(76.8)) {
|
|
.rotate-wrapper {
|
|
.desktop {
|
|
display: none;
|
|
}
|
|
.responsive {
|
|
display: block;
|
|
}
|
|
}
|
|
.rotate-wrapper {
|
|
width: 75%;
|
|
height: 75%;
|
|
}
|
|
.arrow-img {
|
|
height: toRem(4);
|
|
width: toRem(4);
|
|
}
|
|
.arrow-img-1 {
|
|
right: toRem(3);
|
|
top: toRem(4.5);
|
|
}
|
|
.arrow-img-2 {
|
|
left: toRem(2.4);
|
|
bottom: toRem(7.88);
|
|
}
|
|
.car-img {
|
|
height: 40%;
|
|
}
|
|
}
|
|
</style>
|