SuperAPI AI Directory

web-motion-animation

Frontend & websites

View repository

Updated 10-16 07:59

About this skill

Web Motion & Animation Skill

A workflow for adding a modern motion layer to a webpage. Key: motion is a signal, not decoration. It should guide the user's gaze to key information or reinforce hierarchy.

Workflow

Step 1: Choose a background animation (for Hero)

Option A: WebGL Shader Gradient (Stripe·Linear style) — recommended

  • A smoothly flowing color mesh. GPU-accelerated, lightweight
  • Direct GLSL fragment shader without a library
  • Colors based on design-tokens' accent-primary/secondary

Option B: Canvas Particle Network (Apple·Bloomberg style)

  • A network where dots connect and move
  • Suitable as an HBM/AI data flow metaphor
  • canvas 2D, 100–150 particles (50 on mobile)

Option C: Animated Orbs (Vercel·Linear style)

  • 3–5 large blurred circles move slowly and blend colors
  • Possible with CSS alone, but smoother with Canvas

Recommended: Option A (WebGL shader gradient) — modern, smooth, and lightweight. As an alternative, Option B (particle network) also fits the HBM/AI concept well.

Step 2: Implement WebGL Shader Gradient (web/scripts/background-animation.js)

const canvas = document.getElementById('bg-canvas');
const gl = canvas.getContext('webgl');

if (!gl) {
  // Fallback: CSS gradient
  document.body.style.background = 'linear-gradient(135deg, #0a0e1a 0%, #1f2937 100%)';
  // 정적 fallback
} else {
  // Fragment shader (간소화 예시)
  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);
    }
  `;
  
  // 컴파일·렌더 루프
  // ...
}

// prefers-reduced-motion 체크
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);

Alternative: you can use a prebuilt solution such as a shader library (e.g., https://cdn.jsdelivr.net/npm/shaderlib)나 whatamesh ).

Step 3: Scroll entrance animation (web/scripts/scroll-animations.js)

Use 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);

// 섹션 진입 시 fade + slide-up
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',
    },
  });
});

// Hero 텍스트 reveal (단어 단위)
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',
});

// 메타 메트릭 카드 stagger
gsap.from('.meta-metrics .metric-card', {
  y: 40,
  opacity: 0,
  duration: 0.8,
  stagger: 0.15,
  delay: 0.6,
  ease: 'power3.out',
});

// Pin & scrub 예시 (분석 영역에서 시각화 잠금)
// 옵션: 시나리오 카드에서 스크롤로 Bear → Base → Bull 전환

Step 4: Micro-interactions (web/scripts/microinteractions.js)

// 숫자 카운트업 (IntersectionObserver + RAF)
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);  // easeOutCubic
    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));

// 카드 3D tilt on hover
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 = '';
  });
});

// 텍스트 reveal on scroll (대안: SplitType + GSAP)

Step 5: CSS animation utilities (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; }

/* 진입용 fade-up (JS 없이 fallback) */
@keyframes fade-up {
  from { opacity: 0; transform: translateY(40px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Reduced motion 강제 비활성화 */
@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: Measure performance & write motion-notes

web/_design/motion-notes.mdOrganize in:

  • Libraries and versions used (GSAP, ScrollTrigger CDN URL)
  • Reason for choosing the background option (WebGL shader gradient)
  • Trigger, duration, and easing for each motion
  • Performance measurements (expected fps, GPU usage)
  • prefers-reduced-motion handling items
  • Fallback (CSS gradient when WebGL is unsupported)

Working principles

  • Motion = signal: Use only to reinforce key metrics, conversions, and hierarchy. No meaningless decoration
  • Performance first: Maintain 60fps. Simplify under WebGL load. Use requestAnimationFrame
  • Enforce prefers-reduced-motion: Disable all non-essential motion (background canvas also static)
  • Layer separation: position: fixed; z-index: -1to separate background and content
  • Dark/light adaptation: Automatically switch based on design-tokens' color variables
  • Memory management: Pause off-screen motion with IntersectionObserver
  • Follow motion tokens: Use design-lead's duration and easing

Team communication

  • Receive motion tokens, background concept, and key metric emphasis locations from design-lead
  • SendMessage to frontend-engineer with background canvas container spec (e.g., <canvas id="bg-canvas">) and trigger class (.section, [data-countup], [data-stagger]) spec
  • Coordinate chart entry stagger with visualization-engineer
  • Provide motion-notes.md to web-qa + request reduced-motion and performance verification

On follow-up execution

  • Feedback that motion looks excessive → reduce intensity (shorten duration, weaker opacity, reduce scale)
  • Feedback that it feels monotonous → add variations (different easing, adjust stagger)
  • Preserve previous motion JS, modify only the changed areas