바이브코딩으로 제작한 웹아트 시리즈를 새롭게 시작하게 되었어요. 그 첫 번째 작품으로 인터랙티브한 전구 효과를 구현해 보았어요.

이 작품의 핵심은 마우스 커서를 전구로 변환시켜, 어두운 화면 위를 움직일 때마다 숨겨진 단어들이 은은한 빛과 함께 드러나는 시각적 경험을 제공하는 거예요. 마치 실제 전구를 들고 어두운 공간을 탐험하는 듯한 몰입감을 선사해요.
이번 작품은 별도의 라이브러리나 프레임워크 없이 순수 HTML, CSS, JavaScript만을 활용하여 구현했다는 점에서 의미가 있어요. 이는 기술만으로도 충분히 창의적이고 감각적인 인터랙션을 만들어낼 수 있음을 보여주고 있어요.
제공된 소스코드는 웹디자이너와 개발자 모두에게 유용한 자료가 될 거예요. 포트폴리오 사이트의 인트로 페이지, 미스터리한 분위기의 랜딩 페이지, 또는 스토리텔링이 필요한 브랜드 사이트 등 다양한 프로젝트에 응용할 수 있어요. 간단한 코드 수정만으로도 자신만의 독특한 인터랙티브 경험을 만들어낼 수 있을 거예요.
< 소스코드 >
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Monochrome Galaxy</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
background: #000000;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
cursor: none;
position: relative;
}
/* 은하수 배경 레이어 */
.galaxy-layer {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
}
/* 은하수 구름 효과 */
.milky-way {
position: absolute;
width: 150%;
height: 150%;
top: -25%;
left: -25%;
background:
radial-gradient(ellipse at 20% 30%, rgba(255,255,255,0.03) 0%, transparent 50%),
radial-gradient(ellipse at 60% 70%, rgba(255,255,255,0.02) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(255,255,255,0.03) 0%, transparent 40%),
radial-gradient(ellipse at 40% 40%, rgba(255,255,255,0.02) 0%, transparent 50%);
transform: rotate(-20deg);
opacity: 0.7;
}
/* 별 종류별 스타일 */
.star-tiny {
position: absolute;
width: 1px;
height: 1px;
background: white;
border-radius: 50%;
opacity: 0.9;
}
.star-small {
position: absolute;
width: 2px;
height: 2px;
background: white;
border-radius: 50%;
opacity: 0.8;
box-shadow: 0 0 2px rgba(255,255,255,0.8);
}
.star-medium {
position: absolute;
width: 3px;
height: 3px;
background: white;
border-radius: 50%;
opacity: 0.7;
box-shadow: 0 0 4px rgba(255,255,255,0.6);
}
.star-bright {
position: absolute;
width: 4px;
height: 4px;
background: white;
border-radius: 50%;
animation: star-pulse 3s ease-in-out infinite;
}
@keyframes star-pulse {
0%, 100% {
opacity: 0.4;
box-shadow: 0 0 6px rgba(255,255,255,0.9);
}
50% {
opacity: 1;
box-shadow: 0 0 10px rgba(255,255,255,1);
}
}
.star-twinkle {
animation: twinkle 4s ease-in-out infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 0.2; }
50% { opacity: 1; }
}
/* 메인 컨텐츠 */
.container {
position: relative;
z-index: 10;
white-space: nowrap;
}
.hidden-text {
font-family: 'Bebas Neue', sans-serif;
font-size: clamp(4rem, 10vw, 8rem);
letter-spacing: 0.3em;
color: #ffffff;
position: relative;
user-select: none;
opacity: 0;
white-space: nowrap;
}
.reveal-text {
font-family: 'Bebas Neue', sans-serif;
font-size: clamp(4rem, 10vw, 8rem);
letter-spacing: 0.3em;
position: absolute;
top: 0;
left: 0;
color: #ffffff;
user-select: none;
white-space: nowrap;
clip-path: circle(0px at 0px 0px);
}
.letter {
display: inline-block;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 달 조명 */
.moon-light {
position: fixed;
width: 300px;
height: 300px;
pointer-events: none;
z-index: 9999;
left: 0;
top: 0;
transform: translate(-50%, -50%);
}
.moon-core {
position: absolute;
width: 50px;
height: 50px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: radial-gradient(circle at 30% 30%, #ffffff, #e0e0e0);
border-radius: 50%;
box-shadow:
0 0 30px rgba(255,255,255,0.8),
0 0 60px rgba(255,255,255,0.4),
0 0 90px rgba(255,255,255,0.2);
z-index: 2;
}
.moon-core::after {
content: '';
position: absolute;
top: -3px;
right: -8px;
width: 60%;
height: 60%;
border-radius: 50%;
background: #000000;
}
.light-beam {
position: absolute;
width: 300px;
height: 300px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: radial-gradient(
circle,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.05) 30%,
rgba(255, 255, 255, 0.02) 50%,
transparent 70%
);
border-radius: 50%;
animation: pulse 3s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.6;
}
50% {
transform: translate(-50%, -50%) scale(1.05);
opacity: 0.8;
}
}
/* 우주 먼지 */
.space-dust {
position: fixed;
width: 1px;
height: 1px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
pointer-events: none;
animation: float-dust 6s ease-in-out infinite;
}
@keyframes float-dust {
0%, 100% {
transform: translateY(0) translateX(0);
opacity: 0;
}
10% {
opacity: 0.5;
}
90% {
opacity: 0.5;
}
50% {
transform: translateY(-40px) translateX(20px);
}
}
/* 클릭 효과 */
.nova-burst {
position: fixed;
pointer-events: none;
width: 4px;
height: 4px;
background: white;
border-radius: 50%;
animation: nova 1s ease-out forwards;
}
@keyframes nova {
0% {
transform: scale(0);
opacity: 1;
box-shadow: 0 0 10px white;
}
100% {
transform: scale(20);
opacity: 0;
box-shadow: 0 0 2px white;
}
}
/* 글자 발견 효과 */
.discovered {
animation: glow-mono 0.5s ease-out;
}
@keyframes glow-mono {
0% {
text-shadow:
0 0 10px rgba(255, 255, 255, 0);
}
50% {
text-shadow:
0 0 20px rgba(255, 255, 255, 1),
0 0 40px rgba(255, 255, 255, 0.5);
}
100% {
text-shadow:
0 0 10px rgba(255, 255, 255, 0.3);
}
}
/* 힌트 텍스트 */
.hint-text {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
color: #333333;
font-family: 'Bebas Neue', sans-serif;
font-size: 0.9rem;
letter-spacing: 0.2em;
animation: hint-fade 3s ease-in-out infinite;
}
@keyframes hint-fade {
0%, 100% { opacity: 0.2; }
50% { opacity: 0.5; }
}
/* 별똥별 효과 */
.shooting-star {
position: fixed;
width: 2px;
height: 2px;
background: white;
border-radius: 50%;
pointer-events: none;
animation: shoot 2s linear forwards;
}
.shooting-star::after {
content: '';
position: absolute;
width: 100px;
height: 1px;
background: linear-gradient(90deg, white, transparent);
right: 0;
top: 50%;
transform: translateY(-50%);
}
@keyframes shoot {
0% {
transform: translate(0, 0);
opacity: 1;
}
100% {
transform: translate(-500px, 200px);
opacity: 0;
}
}
</style>
</head>
<body>
<!-- 은하수 배경 -->
<div class="galaxy-layer">
<div class="milky-way"></div>
</div>
<!-- 메인 텍스트 -->
<div class="container">
<div class="hidden-text">THREADS</div>
<div class="reveal-text" id="revealText">
<span class="letter">T</span>
<span class="letter">H</span>
<span class="letter">R</span>
<span class="letter">E</span>
<span class="letter">A</span>
<span class="letter">D</span>
<span class="letter">S</span>
</div>
</div>
<!-- 달 커서 -->
<div class="moon-light" id="moonLight">
<div class="light-beam"></div>
<div class="moon-core"></div>
</div>
<!-- 힌트 -->
<div class="hint-text">EXPLORE THE DARKNESS</div>
<script>
const moonLight = document.getElementById('moonLight');
const revealText = document.getElementById('revealText');
const letters = document.querySelectorAll('.letter');
const galaxyLayer = document.querySelector('.galaxy-layer');
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
let currentX = window.innerWidth / 2;
let currentY = window.innerHeight / 2;
// 텍스트 컨테이너 위치
const textRect = revealText.getBoundingClientRect();
// 은하수 별 생성
function createGalaxyStars() {
// 작은 별들 (가장 많음)
for(let i = 0; i < 300; i++) {
const star = document.createElement('div');
star.className = 'star-tiny';
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
star.style.opacity = 0.3 + Math.random() * 0.7;
galaxyLayer.appendChild(star);
}
// 작은 별들
for(let i = 0; i < 150; i++) {
const star = document.createElement('div');
star.className = 'star-small';
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
star.style.opacity = 0.4 + Math.random() * 0.6;
if(Math.random() > 0.7) star.classList.add('star-twinkle');
star.style.animationDelay = Math.random() * 4 + 's';
galaxyLayer.appendChild(star);
}
// 중간 별들
for(let i = 0; i < 80; i++) {
const star = document.createElement('div');
star.className = 'star-medium';
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
star.style.opacity = 0.5 + Math.random() * 0.5;
if(Math.random() > 0.5) star.classList.add('star-twinkle');
star.style.animationDelay = Math.random() * 4 + 's';
galaxyLayer.appendChild(star);
}
// 밝은 별들
for(let i = 0; i < 20; i++) {
const star = document.createElement('div');
star.className = 'star-bright';
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
star.style.animationDelay = Math.random() * 3 + 's';
galaxyLayer.appendChild(star);
}
}
createGalaxyStars();
// 별똥별 생성
function createShootingStar() {
if(Math.random() > 0.98) {
const shootingStar = document.createElement('div');
shootingStar.className = 'shooting-star';
shootingStar.style.left = (50 + Math.random() * 50) + '%';
shootingStar.style.top = Math.random() * 30 + '%';
document.body.appendChild(shootingStar);
setTimeout(() => shootingStar.remove(), 2000);
}
}
// 부드러운 마우스 추적
function animate() {
currentX += (mouseX - currentX) * 0.12;
currentY += (mouseY - currentY) * 0.12;
// 달 위치 업데이트
moonLight.style.left = currentX + 'px';
moonLight.style.top = currentY + 'px';
// 텍스트 마스크 영역 업데이트
const radius = 150;
const relativeX = currentX - textRect.left;
const relativeY = currentY - textRect.top;
revealText.style.clipPath = `circle(${radius}px at ${relativeX}px ${relativeY}px)`;
// 글자별 효과
letters.forEach((letter, index) => {
const rect = letter.getBoundingClientRect();
const letterCenterX = rect.left + rect.width / 2;
const letterCenterY = rect.top + rect.height / 2;
const distance = Math.sqrt(
Math.pow(currentX - letterCenterX, 2) +
Math.pow(currentY - letterCenterY, 2)
);
if(distance < radius) {
const intensity = 1 - (distance / radius);
letter.style.transform = `translateY(${-intensity * 10}px) scale(${1 + intensity * 0.15})`;
letter.style.filter = `brightness(${1 + intensity * 0.5})`;
letter.style.textShadow = `
0 0 ${10 + intensity * 20}px rgba(255, 255, 255, ${0.6 + intensity * 0.4}),
0 0 ${20 + intensity * 40}px rgba(255, 255, 255, ${0.3 + intensity * 0.2})
`;
if(!letter.classList.contains('discovered')) {
letter.classList.add('discovered');
setTimeout(() => letter.classList.remove('discovered'), 500);
}
} else {
letter.style.transform = 'translateY(0) scale(1)';
letter.style.filter = 'brightness(1)';
letter.style.textShadow = 'none';
}
});
// 별똥별 랜덤 생성
createShootingStar();
requestAnimationFrame(animate);
}
animate();
// 마우스 움직임 추적
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
// 우주 먼지 생성
if(Math.random() > 0.97) {
createSpaceDust(e.clientX, e.clientY);
}
});
// 우주 먼지 생성
function createSpaceDust(x, y) {
const dust = document.createElement('div');
dust.className = 'space-dust';
dust.style.left = (x + (Math.random() - 0.5) * 100) + 'px';
dust.style.top = (y + (Math.random() - 0.5) * 100) + 'px';
dust.style.animationDelay = Math.random() * 3 + 's';
document.body.appendChild(dust);
setTimeout(() => dust.remove(), 6000);
}
// 클릭 시 노바 효과
document.addEventListener('click', (e) => {
// 중심 폭발
const nova = document.createElement('div');
nova.className = 'nova-burst';
nova.style.left = e.clientX + 'px';
nova.style.top = e.clientY + 'px';
document.body.appendChild(nova);
setTimeout(() => nova.remove(), 1000);
// 파편 생성
for(let i = 0; i < 12; i++) {
setTimeout(() => {
const angle = (Math.PI * 2 / 12) * i;
const distance = 30 + Math.random() * 50;
createStarFragment(
e.clientX + Math.cos(angle) * distance,
e.clientY + Math.sin(angle) * distance
);
}, i * 20);
}
});
// 별 파편 생성
function createStarFragment(x, y) {
const fragment = document.createElement('div');
fragment.style.position = 'fixed';
fragment.style.left = x + 'px';
fragment.style.top = y + 'px';
fragment.style.width = '3px';
fragment.style.height = '3px';
fragment.style.background = 'white';
fragment.style.borderRadius = '50%';
fragment.style.pointerEvents = 'none';
fragment.style.animation = 'float-dust 2s ease-out forwards';
fragment.style.boxShadow = '0 0 6px rgba(255,255,255,0.8)';
document.body.appendChild(fragment);
setTimeout(() => fragment.remove(), 2000);
}
// 마우스 enter/leave 효과
document.addEventListener('mouseleave', () => {
moonLight.style.opacity = '0.2';
});
document.addEventListener('mouseenter', () => {
moonLight.style.opacity = '1';
});
// 초기 위치 설정
setTimeout(() => {
mouseX = window.innerWidth / 2;
mouseY = window.innerHeight / 2;
}, 100);
// 화면 리사이즈 대응
window.addEventListener('resize', () => {
const newTextRect = revealText.getBoundingClientRect();
textRect.left = newTextRect.left;
textRect.top = newTextRect.top;
});
</script>
</body>
</html>