| name | web-motion-animation |
| description | Canvas/WebGL 배경 애니메이션(particle network, gradient mesh, animated orbs, shader gradient), 스크롤 기반 진입 애니메이션, 마이크로 인터랙션(숫자 카운트업, 텍스트 reveal, 카드 호버 tilt), prefers-reduced-motion 대응을 구현하는 모션 애니메이션 스킬. 배경 모션·페럴랙스·스크롤 애니메이션·마이크로 인터랙션 요청 시 반드시 사용. |
Web Motion & Animation Skill
웹페이지에 모던한 모션 레이어를 추가하는 워크플로우. 핵심: 모션은 장식이 아닌 시그널. 사용자 시선을 핵심 정보로 유도하거나 위계를 강화해야 한다.
워크플로우
Step 1: 배경 애니메이션 선택 (Hero용)
옵션 A: WebGL Shader Gradient (Stripe·Linear 스타일) — 권장
- 부드럽게 흐르는 색 메시. GPU 가속, 가벼움
- 라이브러리 없이 직접 GLSL fragment shader
- 색상은 design-tokens의 accent-primary/secondary 기반
옵션 B: Canvas Particle Network (Apple·Bloomberg 스타일)
- 점들이 연결되며 움직이는 네트워크
- HBM/AI 데이터 흐름 메타포로 적합
- canvas 2D, 100~150 파티클 (모바일 50)
옵션 C: Animated Orbs (Vercel·Linear 스타일)
- 3~5개 큰 블러 원이 천천히 움직이며 색 혼합
- CSS만으로도 가능하지만 Canvas로 부드럽게
권장: 옵션 A (WebGL shader gradient) — 모던하고 부드럽고 가벼움.
대안으로 옵션 B (particle network)도 HBM/AI 컨셉과 잘 맞음.
Step 2: WebGL Shader Gradient 구현 (web/scripts/background-animation.js)
const canvas = document.getElementById('bg-canvas');
const gl = canvas.getContext('webgl');
if (!gl) {
document.body.style.background = 'linear-gradient(135deg, #0a0e1a 0%, #1f2937 100%)';
} else {
const fragmentSource = `
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
// simplex noise function (생략, GLSL 표준)
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
float t = u_time * 0.0001;
// 3개의 천천히 움직이는 색상 점
vec3 color1 = vec3(0.024, 0.071, 0.102); // bg-primary
vec3 color2 = vec3(0.024, 0.71, 0.83); // accent-primary (cyan)
vec3 color3 = vec3(0.545, 0.361, 0.965); // accent-secondary (purple)
float n1 = snoise(vec3(uv * 2.0, t)) * 0.5 + 0.5;
float n2 = snoise(vec3(uv * 3.0, t + 100.0)) * 0.5 + 0.5;
vec3 color = mix(color1, color2, n1 * 0.3);
color = mix(color, color3, n2 * 0.2);
gl_FragColor = vec4(color, 1.0);
}
`;
}
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
cancelAnimationFrame(animationId);
document.body.style.background = 'radial-gradient(circle at 30% 20%, rgba(6,182,212,0.1) 0%, transparent 50%), var(--bg-primary)';
}
const ioBg = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) startAnim(); else pauseAnim();
});
ioBg.observe(canvas);
대안: shader 라이브러리(예: https://cdn.jsdelivr.net/npm/shaderlib)나 whatamesh 같은 사전 빌드 솔루션 활용 가능.
Step 3: 스크롤 진입 애니메이션 (web/scripts/scroll-animations.js)
GSAP ScrollTrigger 활용 (CDN: cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js + dist/ScrollTrigger.min.js):
import gsap from 'https://cdn.skypack.dev/gsap';
import { ScrollTrigger } from 'https://cdn.skypack.dev/gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
gsap.utils.toArray('.section').forEach((section) => {
gsap.from(section.querySelectorAll('h2, p, .summary-card, .chart-container'), {
y: 60,
opacity: 0,
duration: 0.8,
ease: 'power3.out',
stagger: 0.1,
scrollTrigger: {
trigger: section,
start: 'top 75%',
toggleActions: 'play none none reverse',
},
});
});
const heroTitle = document.querySelector('.hero-title .display-1');
const words = heroTitle.textContent.split('').map(c => `<span class="char">${c}</span>`).join('');
heroTitle.innerHTML = words;
gsap.from('.hero-title .char', {
y: 120,
opacity: 0,
duration: 1,
stagger: 0.04,
ease: 'power4.out',
});
gsap.from('.meta-metrics .metric-card', {
y: 40,
opacity: 0,
duration: 0.8,
stagger: 0.15,
delay: 0.6,
ease: 'power3.out',
});
Step 4: 마이크로 인터랙션 (web/scripts/microinteractions.js)
function countUp(el, target, duration = 1500, formatter) {
const start = 0;
const startTime = performance.now();
function tick(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const value = start + (target - start) * eased;
el.textContent = formatter ? formatter(value) : Math.round(value).toLocaleString('ko-KR');
if (progress < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
const countUpObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !entry.target.dataset.counted) {
entry.target.dataset.counted = 'true';
const target = parseFloat(entry.target.dataset.countup);
countUp(entry.target, target);
}
});
}, { threshold: 0.6 });
document.querySelectorAll('[data-countup]').forEach(el => countUpObserver.observe(el));
document.querySelectorAll('.metric-card, .summary-card').forEach(card => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
card.style.transform = `perspective(1000px) rotateY(${x * 6}deg) rotateX(${-y * 6}deg) translateY(-2px)`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
});
});
Step 5: CSS 애니메이션 유틸리티 (web/styles/animations.css)
@keyframes pulse-down {
0%, 100% { transform: translateY(0); opacity: 0.6; }
50% { transform: translateY(8px); opacity: 1; }
}
.scroll-indicator { animation: pulse-down 2s ease-in-out infinite; }
@keyframes glow {
0%, 100% { box-shadow: 0 0 32px rgba(6,182,212,0.2); }
50% { box-shadow: 0 0 48px rgba(6,182,212,0.4); }
}
.metric-card.highlight { animation: glow 3s ease-in-out infinite; }
@keyframes fade-up {
from { opacity: 0; transform: translateY(40px); }
to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
#bg-canvas { display: none; }
}
Step 6: 성능 측정 & motion-notes 작성
web/_design/motion-notes.md에 정리:
- 사용 라이브러리·버전 (GSAP, ScrollTrigger CDN URL)
- 배경 옵션 선택 사유 (WebGL shader gradient)
- 각 모션의 트리거·duration·easing
- 성능 측정값 (예상 fps, GPU 사용)
- prefers-reduced-motion 대응 항목
- Fallback (WebGL 비지원 시 CSS gradient)
작업 원칙
- 모션 = 시그널: 핵심 메트릭·전환·위계 강화에만 사용. 의미 없는 장식 금지
- 성능 우선: 60fps 유지. WebGL 부하 시 단순화. requestAnimationFrame 사용
- prefers-reduced-motion 강제: 모든 비필수 모션 비활성화 (배경 캔버스도 정적으로)
- 레이어 분리:
position: fixed; z-index: -1로 배경과 콘텐츠 분리
- 다크/라이트 적응: design-tokens의 색상 변수 기반으로 자동 전환
- 메모리 관리: IntersectionObserver로 화면 밖 모션 일시 중지
- 모션 토큰 준수: design-lead의 duration·easing 사용
팀 통신
- design-lead로부터 모션 토큰·배경 컨셉·핵심 메트릭 강조 위치 수신
- frontend-engineer에게 배경 캔버스 컨테이너 명세 (예:
<canvas id="bg-canvas">)와 트리거 클래스(.section, [data-countup], [data-stagger]) 명세 SendMessage
- visualization-engineer와 차트 진입 stagger 조율
- web-qa에게 motion-notes.md 제공 + reduced-motion·성능 검증 요청
후속 실행 시
- 모션이 과해 보인다는 피드백 → 강도 낮춤 (duration 단축, opacity 약함, scale 줄임)
- 단조롭다는 피드백 → 변형 추가 (다른 easing, 스태거 조정)
- 이전 motion JS 보존, 변경 영역만 수정