
제 블로그를 보시는 분들은 아시겠지만
마우스를 클릭하면, 뚱이가 비눗방울을 내뿜는데요.
그 코드를 지금 바로 공개합니다.
두둥
설정 - 스킨편집 - html 편집 들어가셔서 수정하시면 됩니다.


1. html body 최하단 삽입
- 위에가 주석 없는 버전, 아래가 주석 있는 버전입니다. 같은 코드에요.
<script>
(function () {
if (window.__bubbleBound) return;
window.__bubbleBound = true;
function spawnBubble(x, y) {
const el = document.createElement('div');
el.className = 'bubble';
const size = 25 + Math.random() * 60;
const dx = (Math.random() - 0.5) * 90;
const dy = -60 - Math.random() * 70;
const dur = 700 + Math.random() * 900;
el.style.setProperty('--x', x);
el.style.setProperty('--y', y);
el.style.setProperty('--size', size);
el.style.setProperty('--dx', dx + 'px');
el.style.setProperty('--dy', dy + 'px');
el.style.setProperty('--dur', dur);
document.body.appendChild(el);
el.addEventListener('animationend', () => el.remove());
}
function onPointer(e) {
const x = e.clientX ?? (e.touches && e.touches[0]?.clientX);
const y = e.clientY ?? (e.touches && e.touches[0]?.clientY);
if (typeof x !== 'number' || typeof y !== 'number') return;
const count = 6 + Math.floor(Math.random() * 5);
for (let i = 0; i < count; i++) {
setTimeout(() => spawnBubble(
x + (Math.random()-0.5)*70,
y + (Math.random()-0.5)*70
), i * 70);
}
}
document.addEventListener('pointerdown', onPointer, { passive: true, capture: true });
document.addEventListener('touchstart', e => {
const t = e.touches && e.touches[0];
if (!t) return;
onPointer({ clientX: t.clientX, clientY: t.clientY });
}, { passive: true, capture: true });
})();
</script>
<script>
(function () { // 즉시 실행 함수(IIFE): 전역 오염 없이 한 번만 초기화
if (window.__bubbleBound) return; // 이미 바인딩된 적 있으면 중복 초기화 방지
window.__bubbleBound = true; // 한 번 바인딩했음을 표시하는 플래그
function spawnBubble(x, y) { // 비눗방울 하나 생성하는 함수
const el = document.createElement('div'); // div 요소 생성
el.className = 'bubble'; // CSS에서 스타일링할 클래스 지정
const size = 25 + Math.random() * 60; // 버블 크기(25~85px) 랜덤
const dx = (Math.random() - 0.5) * 90; // 가로로 살짝 흩어지는 이동량(-45~+45px)
const dy = -60 - Math.random() * 70; // 위쪽으로 떠오를 이동량(-60~-130px)
const dur = 1000 + Math.random() * 900; // 애니메이션 지속시간(700~1600ms)
el.style.setProperty('--x', x); // 시작 X 좌표를 CSS 변수로 전달
el.style.setProperty('--y', y); // 시작 Y 좌표를 CSS 변수로 전달
el.style.setProperty('--size', size); // 크기를 CSS 변수로 전달
el.style.setProperty('--dx', dx + 'px'); // 가로 이동량을 CSS 변수로 전달
el.style.setProperty('--dy', dy + 'px'); // 세로 이동량을 CSS 변수로 전달
el.style.setProperty('--dur', dur); // 애니메이션 시간(ms)을 CSS 변수로 전달
document.body.appendChild(el); // 문서에 버블 추가 → 애니메이션 시작
el.addEventListener('animationend', () => el.remove()); // 애니메이션 끝나면 DOM에서 제거(누적 방지)
}
function onPointer(e) { // 포인터(마우스/펜/터치) 입력 핸들러
const x = e.clientX ?? (e.touches && e.touches[0]?.clientX); // 입력 이벤트에서 X 좌표 추출(터치 호환)
const y = e.clientY ?? (e.touches && e.touches[0]?.clientY); // 입력 이벤트에서 Y 좌표 추출
if (typeof x !== 'number' || typeof y !== 'number') return; // 좌표 없으면 종료
const count = 6 + Math.floor(Math.random() * 5); // 한 번에 생성할 버블 수(6~10개)
for (let i = 0; i < count; i++) { // count만큼 반복 생성
setTimeout(() => spawnBubble( // 약간의 시간차를 두고 생성(폭죽처럼 퍼짐)
x + (Math.random() - 0.5) * 70, // 클릭 지점 기준 가로 랜덤 오프셋(-35~+35px)
y + (Math.random() - 0.5) * 70 // 클릭 지점 기준 세로 랜덤 오프셋(-35~+35px)
), i * 70); // i에 비례한 지연(0ms, 70ms, 140ms, …)
}
}
document.addEventListener('pointerdown', onPointer, { passive: true, capture: true });
// 포인터 다운(클릭/펜/터치 시작) 시 onPointer 호출
// passive: 스크롤 성능 최적화(기본 동작 차단 안 함), capture: 캡처 단계에서 먼저 받음(상위에서 가로채여도 동작)
document.addEventListener('touchstart', e => { // 일부 브라우저/상황용 터치 보조 핸들러
const t = e.touches && e.touches[0]; // 첫 번째 터치 포인트
if (!t) return; // 없으면 종료
onPointer({ clientX: t.clientX, clientY: t.clientY }); // 터치 좌표를 포인터 형태로 래핑해 전달
}, { passive: true, capture: true });
})(); // 즉시 실행하여 리스너를 한 번만 세팅
</script>
2. css 최하단에 삽입
- 이건 왜인지 모르게 주석이 계속 깨지는데요.
마찬가지로 위에는 주석 최소화 버전이고 아래는 주석 버전입니다.
/* 비눗방울 효과 시작 !!!!!!!!! */
:root {
--bubble-glow: rgba(255,255,255,0.8);
}
:root[data-theme="light"] {
--dark-bg-color: #ffffff; /* 라이트 배경 */
--dark-font-color: #111111; /* 라이트 텍스트 */
--dark-bg-second-color: #f4f6f8; /* 서브 배경 */
--sidebar-hr-color: rgba(0,0,0,.12);
/* --highlight-color: #ff4081; */
}
/* 비눗방울 본체 */
.bubble {
position: fixed;
left: calc(var(--x) * 1px);
top: calc(var(--y) * 1px);
width: calc(var(--size, 30) * 1px);
height: calc(var(--size, 30) * 1px);
border-radius: 50%;
pointer-events: none;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
z-index: 2147483647;
animation: bubble-rise calc(var(--dur, 1400) * 0.5ms) ease-in-out forwards;
will-change: transform, opacity, filter;
mix-blend-mode: screen;
background:
radial-gradient(35% 35% at 30% 28%,
rgba(255,255,255,.95) 0%,
rgba(255,255,255,.35) 35%,
rgba(255,255,255,0) 60%),
conic-gradient(from 0deg,
rgba(255,180,180,.30),
rgba(255,240,200,.28),
rgba(190,255,220,.28),
rgba(200,220,255,.30),
rgba(255,180,255,.28),
rgba(255,180,180,.30));
background-blend-mode: screen, lighten;
box-shadow:
0 0 25px 6px rgba(255,255,255,0.25),
inset 0 0 25px rgba(255,255,255,0.25);
filter: saturate(1.2) brightness(1.05);
}
/* 하이라이트 */
.bubble::before {
content: "";
position: absolute;
left: 18%;
top: 12%;
width: 40%;
height: 40%;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.7), rgba(255,255,255,0) 70%);
opacity: 0.4;
}
.bubble::after {
content: "";
position: absolute;
left: 60%;
top: 58%;
width: 25%;
height: 25%;
border-radius: 50%;
background: radial-gradient(circle at 50% 50%, rgba(255,255,255,0.5), rgba(255,255,255,0) 80%);
opacity: 0.4;
}
/* 색 변화 + 위로 이동 */
@keyframes bubble-rise {
0% {
transform: translate(-20%, -20%) scale(0.3);
opacity: 0;
filter: hue-rotate(0deg);
}
25% {
transform: translate(-20%, -20%) scale(1.1);
opacity: 1;
filter: hue-rotate(60deg);
}
60% {
transform: translate(calc(-20% + var(--dx, 0px)), calc(-20% + var(--dy, -50px))) scale(0.95);
opacity: 0.95;
filter: hue-rotate(180deg) brightness(1.35);
}
100% {
transform: translate(calc(-20% + var(--dx, 0px)), calc(-20% + var(--dy, -130px))) scale(0.8);
opacity: 0;
filter: hue-rotate(360deg);
}
}
/* 비눗방울 효과 시작 */
:root {
--bubble-glow: rgba(255,255,255,0.8); /* 전체에서 재사용할 기본 발광색(흰빛) 변수 */
}
:root[data-theme="light"] {
--dark-bg-color: #ffffff; /* 라이트 모드 배경색 */
--dark-font-color: #111111; /* 라이트 모드 글자색 */
--dark-bg-second-color: #f4f6f8; /* 서브 배경 */
--sidebar-hr-color: rgba(0,0,0,.12);/* 구분선 색 */
/* --highlight-color: #ff4081; 강조색(현재 미사용) */
}
/* 비눗방울 본체 */
.bubble {
position: fixed; /* 화면 스크롤과 상관없이 고정 위치 */
left: calc(var(--x) * 1px); /* JS로 전달된 X좌표 */
top: calc(var(--y) * 1px); /* JS로 전달된 Y좌표 */
width: calc(var(--size, 30) * 1px); /* 랜덤 크기 (기본 30px) */
height: calc(var(--size, 30) * 1px);
border-radius: 50%; /* 원형으로 만듦 */
pointer-events: none; /* 클릭 이벤트를 막지 않음 */
transform: translate(-50%, -50%) scale(0); /* 중앙 기준으로 작게 시작 */
opacity: 0; /* 처음엔 투명 */
z-index: 2147483647; /* 최상단에 표시 */
animation: bubble-rise calc(var(--dur, 1400) * 0.5ms) ease-in-out forwards;
/* bubble-rise 애니메이션을 실행. 지속시간은 JS에서 지정한 --dur * 0.5ms */
will-change: transform, opacity, filter; /* 애니메이션 성능 향상 힌트 */
mix-blend-mode: screen; /* 배경 위에 밝은 색이 자연스럽게 섞이도록 함 */
/* 비눗방울의 색감과 질감 (2개의 레이어가 섞임) */
background:
radial-gradient(35% 35% at 30% 28%, /* 중심부 반사광 */
rgba(255,255,255,.95) 0%, /* 중심 흰색 강한 반사 */
rgba(255,255,255,.35) 35%, /* 중간 영역: 연한 빛 */
rgba(255,255,255,0) 60%), /* 외곽으로 갈수록 투명 */
conic-gradient(from 0deg, /* 원형으로 퍼지는 오로라 색상 */
rgba(255,180,180,.30), /* 연한 핑크 */
rgba(255,240,200,.28), /* 크림빛 노랑 */
rgba(190,255,220,.28), /* 민트 */
rgba(200,220,255,.30), /* 하늘색 */
rgba(255,180,255,.28), /* 보라핑크 */
rgba(255,180,180,.30)); /* 다시 핑크로 돌아옴 */
background-blend-mode: screen, lighten; /* 위 두 레이어를 밝게 섞음 */
box-shadow:
0 0 25px 6px rgba(255,255,255,0.25), /* 외곽에 은은한 발광 */
inset 0 0 25px rgba(255,255,255,0.25); /* 내부에서 은은히 비치는 빛 */
filter: saturate(1.2) brightness(1.05); /* 약간 더 밝고 선명하게 */
}
/* 상단 왼쪽의 반사광 */
.bubble::before {
content: "";
position: absolute;
left: 18%; /* 버블 내 상대 위치 */
top: 12%;
width: 40%;
height: 40%;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, /* 작게 흰 점 반사 */
rgba(255,255,255,0.7),
rgba(255,255,255,0) 70%);
opacity: 0.4; /* 투명하게 빛나게 */
}
/* 하단 오른쪽의 은은한 반사광 */
.bubble::after {
content: "";
position: absolute;
left: 60%; /* 버블 내부 위치 */
top: 58%;
width: 25%;
height: 25%;
border-radius: 50%;
background: radial-gradient(circle at 50% 50%, /* 옅은 하이라이트 */
rgba(255,255,255,0.5),
rgba(255,255,255,0) 80%);
opacity: 0.4;
}
/* 버블이 위로 떠오르며 색이 회전하는 애니메이션 */
@keyframes bubble-rise {
0% {
transform: translate(-20%, -20%) scale(0.3); /* 작게 시작 (왼쪽 위 기준) */
opacity: 0; /* 완전히 투명 */
filter: hue-rotate(0deg); /* 색상 회전 없음 (기본색) */
}
25% {
transform: translate(-20%, -20%) scale(1.1); /* 커지며 나타남 */
opacity: 1; /* 완전히 보이게 */
filter: hue-rotate(60deg); /* 색이 조금 바뀜 */
}
60% {
transform: translate(calc(-20% + var(--dx, 0px)),
calc(-20% + var(--dy, -50px))) scale(0.95);
/* 랜덤 이동 + 위로 조금 올라감 */
opacity: 0.95;
filter: hue-rotate(180deg) brightness(1.35); /* 한층 밝아짐 */
}
100% {
transform: translate(calc(-20% + var(--dx, 0px)),
calc(-20% + var(--dy, -130px))) scale(0.8);
/* 더 높이 올라가며 작아짐 */
opacity: 0; /* 사라짐 */
filter: hue-rotate(360deg); /* 색이 한 바퀴 돌아 원래로 */
}
}
- :root 부분은 테마 변수.
- .bubble은 비눗방울의 모양·질감.
- ::before, ::after는 반사광 효과.
- @keyframes bubble-rise는 위로 떠오르며 색이 도는 애니메이션.
'PROJECT > 기타' 카테고리의 다른 글
| colab 글씨체 변경 법 정리 (1) | 2025.10.10 |
|---|---|
| [AI] OpenAI API KEY 발급받고 폐기하기 + 과금 방지까지! (0) | 2025.09.26 |
| [gitlab] 예전 커밋 (버전) 을 다운로드 하고 싶을 때 (0) | 2025.09.25 |
| [SQLite 설치 오류 해결] PATH 등록했는데도 Git Bash에서 sqlite3 실행이 안 될 때 (0) | 2025.09.17 |
| [티스토리 적응기] html 문법 활용하기 (0) | 2025.09.17 |