모바일 네이버 닷 처럼 원형으로 스와이퍼할 수 있는 기능

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Document</title>
<style>
:root {
--rotate-speed: 0.05s;
}
body { margin: 0; padding: 10px; height:100svh; width:100vw; overflow: hidden; overscroll-behavior: none; }
.circle { width: 360px; height: 360px; background-color: #fff; border-radius: 50%; box-shadow: 0 2px 8px #aaa; position:relative; will-change: transform; transition: ease-in-out var(--rotate-speed); margin-top: calc(100svh - 400px); }
.circle__center { position:absolute; width:60px; height:60px; top:50%; left: 50%; transform: translateX(-50%) translateY(-50%); }
.link { list-style: none; width:100%; height:100%; position: relative; padding:0; margin:0; }
.link__item { position:absolute; width:60px; height:60px; display: flex; justify-content: center; align-items: center; will-change: transform; transition: ease-in-out var(--rotate-speed); font-size:12px; flex-direction: column; }
.link__item div { pointer-events: none; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none }
.link__item div:nth-child(1) { font-size: 50px; line-height: 0.5; }
.link__item:nth-child(1) { top:-120px; left:0px; }
.link__item:nth-child(2) { top:-90px; left:90px; }
.link__item:nth-child(3) { top:0px; left:120px; }
.link__item:nth-child(4) { top:90px; left:90px; }
.link__item:nth-child(5) { top:120px; left:0px; }
.link__item:nth-child(6) { top:90px; left:-90px; }
.link__item:nth-child(7) { top:0px; left:-120px; }
.link__item:nth-child(8) { top:-90px; left:-90px; }
</style>
</head>
<body style="background-color: #ccc;">
<div class="circle" data-rotate="0">
<div class="circle__center">
<ul class="link">
<li class="link__item" data-value="1">
<div>ㆍ</div>
<div>음성</div>
</li>
<li class="link__item" data-value="2">
<div>ㆍ</div>
<div>내주변</div>
</li>
<li class="link__item" data-value="3">
<div>ㆍ</div>
<div>검색</div>
</li>
<li class="link__item" data-value="4">
<div>ㆍ</div>
<div>파파고번역</div>
</li>
<li class="link__item" data-value="5">
<div>ㆍ</div>
<div>쇼핑렌즈</div>
</li>
<li class="link__item" data-value="6">
<div>ㆍ</div>
<div>QR바코드</div>
</li>
<li class="link__item" data-value="7">
<div>ㆍ</div>
<div>렌즈</div>
</li>
<li class="link__item" data-value="8">
<div>ㆍ</div>
<div>음악</div>
</li>
</ul>
</div>
</div>
</body>
</html>
<script>
const isMobile = detectMobileDevice(window.navigator.userAgent)
const circle = document.querySelector('.circle');
const linkItems = circle.querySelectorAll('.link__item');
const center_X = circle.clientWidth/2;
const center_Y = circle.clientHeight/2;
let angle_start = 0; // 터치 시작 시 각도
let angle_before = 0; // 터치 이동 시 이전 각도
let angle_after = 0; // 터치 이동 중 각도
let rotate_start = 0; // 회전 시작
let rotate_end = 0; // 회전 종료
const angle_move_unit = 3.5; // 이동 각도 간격 (클수록 빨라짐)
if(isMobile) {
circle.addEventListener('touchstart', (e) => {
angle_start = 각도구하기(e);
angle_before = angle_start;
rotate_start = Number(circle.dataset.rotate);
});
circle.addEventListener('touchmove', (e) => {
angle_before = angle_after;
angle_after = 각도구하기(e);
if(Math.abs(angle_before - angle_after) > 300){ return; }
if(angle_before < angle_after){
circle.dataset.rotate = Number(circle.dataset.rotate) - angle_move_unit;
} else if(angle_before > angle_after){
circle.dataset.rotate = Number(circle.dataset.rotate) + angle_move_unit;
}
rotate_end = Number(circle.dataset.rotate);
위치조정();
});
circle.addEventListener('touchend', (e) => {
let _angle = circle.dataset.rotate;
const angle_45_value = parseInt( _angle / 45 );
const angle_45_remain = (Math.abs(_angle) % 45);
if(angle_45_remain > 22.5){
_angle = angle_45_value * 45;
if(rotate_start < rotate_end){ _angle += 45; }
if(rotate_start > rotate_end){ _angle -= 45; }
} else {
_angle = angle_45_value * 45;
}
circle.dataset.rotate = Number(_angle);
위치조정();
});
} else {
// circle.addEventListener('mousedown', (e) => {
// console.log('mousedown')
// posX_start = e.pageX;
// PosY_start = e.pageY;
// });
}
const link = document.querySelector('.link');
link.addEventListener('click', (e) => {
alert(`${e.target.dataset.value}번 아이템 클릭`);
})
function 위치조정(){
circle.style.transform = `rotate(${circle.dataset.rotate}deg)`;
linkItems.forEach(el => {
el.style.transform = `rotate(${(-1)*circle.dataset.rotate}deg)`;
})
}
function 각도구하기(e){
const _posX = e.touches[0].clientX - circle.offsetLeft - center_X;
const _posY = (-1)*(e.touches[0].clientY - circle.offsetTop - center_Y);
const radian = Math.atan2(_posY, _posX);
let degree = radian * 180 / Math.PI;
if(degree < 0){
degree += 360;
}
return degree;
}
function detectMobileDevice(agent) {
const mobileRegex = [
/Android/i,
/iPhone/i,
/iPad/i,
/iPod/i,
/BlackBerry/i,
/Windows Phone/i
]
return mobileRegex.some(mobile => agent.match(mobile))
}
</script>