<body>
<div class="displayed-cursor">
<div class="displayed-cursor-circle"></div>
</div>
<div class="container">
<div class="target-element">
<div class="target-element-circle"></div>
</div>
<div class="target-element">
<div class="target-element-circle"></div>
</div>
<div class="target-element">
<div class="target-element-circle"></div>
</div>
<div class="target-element">
<div class="target-element-circle"></div>
</div>
<div class="target-element">
<div class="target-element-circle"></div>
</div>
<div class="target-element">
<div class="target-element-circle"></div>
</div>
<div class="target-element">
<div class="target-element-circle"></div>
</div>
<div class="target-element">
<div class="target-element-circle"></div>
</div>
<div class="target-element">
<div class="target-element-circle"></div>
</div>
</div>
</body>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #000;
min-height: 100vh;
overflow: hidden;
cursor: none;
}
.displayed-cursor {
position: absolute;
top: -10px;
left: -10px;
z-index: 10000;
mix-blend-mode: difference;
pointer-events: none;
transition: transform 0.03s;
}
.displayed-cursor-circle {
position: absolute;
top: 0;
left: 0;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #fff;
transition: transform 0.3s;
}
.container {
color: floralwhite;
display: grid;
grid-template-columns: repeat(3, 1fr);
width: 210px;
height: 210px;
}
.target-element {
display: flex;
flex-flow: row nowrap;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.target-element-circle {
background-color: #eee;
width: 20px;
height: 20px;
border-radius: 50%;
transition: transform 0.2s;
pointer-events: none;
}
.on-target-element {
transform: scale3d(2.5, 2.5, 1);
}
const $displayedCursor = document.querySelector(".displayed-cursor");
const $targetElements = document.querySelectorAll(".target-element");
let $targetElementCircle;
let actualCursorX = 0;
let actualCursorY = 0;
let displayedCursorX = 0;
let displayedCursorY = 0;
let targetElementX = 0;
let targetElementY = 0;
let targetElementWidth = 0;
let targetElementHeight = 0;
let targetElementActualCursorX = 0;
let targetElementActualCursorY = 0;
let targetElementCircleX = 0;
let targetElementCircleY = 0;
let displayedCursorIsSticked = false;
const handleActualCursorPositionSettingInDocument = (event) => {
actualCursorX = event.pageX;
actualCursorY = event.pageY;
};
const handleActualCursorPositionSettingInTargetElement = (event) => {
targetElementActualCursorX = event.offsetX;
targetElementActualCursorY = event.offsetY;
};
const handleDisplayedCursorSticksToTargetElement = (event) => {
targetElementX = event.target.offsetLeft;
targetElementY = event.target.offsetTop;
targetElementWidth = event.target.offsetWidth;
targetElementHeight = event.target.offsetHeight;
$targetElementCircle = event.target.firstElementChild;
displayedCursorIsSticked = true;
triggerDisplayedCursorStickAnimation();
};
const handleDisplayedCursorDeviateFromTargetElement = () => {
displayedCursorIsSticked = false;
triggerDisplayedCursorAnimation();
};
const triggerDisplayedCursorAnimation = (timestamp) => {
if (!displayedCursorIsSticked) {
requestAnimationFrame(triggerDisplayedCursorAnimation);
}
// 표시 커서가 실제 커서를 따라다니는 효과.
displayedCursorX -= (displayedCursorX - actualCursorX) * 0.8;
displayedCursorY -= (displayedCursorY - actualCursorY) * 0.8;
$displayedCursor.style.transform = `translate3D(${displayedCursorX}px, ${displayedCursorY}px, 0)`;
// 표시 커서가 작아지는 효과.
$displayedCursor.firstElementChild.classList.remove("on-target-element");
// 목표 요소가 제자리로 돌아가는 효과.
targetElementCircleX = 0;
targetElementCircleY = 0;
if ($targetElementCircle) {
$targetElementCircle.style.transform = `translate3D(${targetElementCircleX}px, ${targetElementCircleY}px, 0)`;
}
};
const triggerDisplayedCursorStickAnimation = (timestamp) => {
if (displayedCursorIsSticked) {
requestAnimationFrame(triggerDisplayedCursorStickAnimation);
}
// 표시 커서가 목표 요소 중앙으로 이동하는 동시에, 실제 커서 위치에 반응해 끌려가는 효과.
displayedCursorX -=
(displayedCursorX -
targetElementX -
targetElementWidth / 2 -
(targetElementActualCursorX - targetElementWidth / 2) * 0.2) *
0.2;
displayedCursorY -=
(displayedCursorY -
targetElementY -
targetElementHeight / 2 -
(targetElementActualCursorY - targetElementHeight / 2) * 0.2) *
0.2;
$displayedCursor.style.transform = `translate3D(${displayedCursorX}px, ${displayedCursorY}px, 0)`;
// 표시 커서 크기가 커지는 효과.
$displayedCursor.firstElementChild.classList.add("on-target-element");
// 목표 요소가 실제 커서 위치에 반응해 끌려가는 효과.
targetElementCircleX =
(targetElementActualCursorX - targetElementWidth / 2) * 0.2;
targetElementCircleY =
(targetElementActualCursorY - targetElementHeight / 2) * 0.2;
$targetElementCircle.style.transform = `translate3D(${targetElementCircleX}px, ${targetElementCircleY}px, 0)`;
};
document.addEventListener(
"mousemove",
handleActualCursorPositionSettingInDocument
);
$targetElements.forEach(($targetElement) => {
$targetElement.addEventListener(
"mousemove",
handleActualCursorPositionSettingInTargetElement
);
});
$targetElements.forEach(($targetElement) => {
$targetElement.addEventListener(
"mouseenter",
handleDisplayedCursorSticksToTargetElement
);
});
$targetElements.forEach(($targetElement) => {
$targetElement.addEventListener(
"mouseleave",
handleDisplayedCursorDeviateFromTargetElement
);
});
triggerDisplayedCursorAnimation();
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.