반응형

AI가 발전되어
이제 canvas나 svg tag를 직접 조작할수 있는 레벨까지 온거 같네요.
 
이 글에서는 svg에 대해 간단히 설명하고 "Figma"에서 svg를 직접 추출해내어 react로 수작업으로 progressbar를 만들며 간단한 인터렉션을 넣어봅시다.
 

SVG 간단 설명

SVG는 Scalable Vector Graphics의 약자로, 크기를 늘리거나 줄여도 화질이 깨지지 않는 벡터 이미지 포맷이에요
 
쉽게 말해, 그림을 수학적인 좌표와 도형으로 표현하는 언어이지요. 그래서 그림을 태그(tag)와 속성으로 작성하게 되는데, HTML이 <div>나 <p> 같은 태그로 문서를 구조화하듯, SVG도 여러 태그를 사용해요. 겁먹을 필요 없어요. 하나씩 알아볼까요?
 

  • <svg>: 모든 SVG 코드의 뿌리, 도화지 역할을 하는 태그예요. 이 태그 안에 우리가 그리고 싶은 도형들을 넣으면 돼요.
  • <g>: 그룹(group)을 만드는 태그예요. 여러 도형을 <g>로 묶으면 한꺼번에 이동하거나 스타일을 같이 적용하기 편해요. 폴더처럼 묶어준다고 생각하면 쉬워요.
  • <rect>: 사각형(rectangle)을 그리는 태그예요. x, y 좌표와 width, height 속성을 주면 해당 위치에 네모를 그려줘요. 색을 채우고 싶다면 fill 속성으로 색상을 지정하면 돼요.
  • <path>: 가장 만능 도형 그리기 태그예요. 직선, 곡선 등 복잡한 모양은 <path>의 d 속성에 특별한 문자열(그림 그리는 명령어들)을 넣어서 표현해요. 마치 점 잇기 놀이로 그림을 그린다고 상상해보세요.
  • <linearGradient>: 예쁘게 색을 섞어주는 그라디언트(gradient) 효과를 정의하는 태그예요. 시작 색부터 끝 색까지 점차 변하는 색깔띠를 만들 수 있어요. 이 태그는 주로 <defs> 안에 넣어서 정의하고, 나중에 도형에서 fill="url(#그라디언트ID)"처럼 불러내 써요.
  • <stop>: 방금 말한 그라디언트에서 색이 바뀌는 지점을 지정하는 태그예요. offset 속성으로 위치(0%~100%)를 정하고 stop-color로 색을 정해요. 여러 개의 <stop>을 넣으면 색이 여러 번 변할 수도 있어요.
  • <clipPath>: 그림을 자르는 액자 같은 역할이에요. <clipPath> 안에 특정 모양을 그려놓고 다른 도형에 적용하면, 그 모양 안쪽 부분만 보이고 바깥 부분은 잘려 보이게 돼요. 꼭 종이를 오려서 겹쳐놓은 것처럼요.
  • <mask>: 마스크는 투명한 필름지처럼 부분 투명하거나 가려주는 효과를 줄 때 써요. 예를 들어, <mask> 안에 검정~흰색 그라디언트를 넣고 어떤 이미지에 씌우면, 천천히 사라지는 페이드 효과를 낼 수 있어요.
  • <filter>: 블러(흐리게)나 그림자 같은 특수 효과를 줄 때 사용해요. 포토샵의 필터를 떠올리시면 돼요. <filter> 안에 종류와 강도를 지정하고 도형에 적용하면, 그림자가 생기거나 반짝거리게 만들 수도 있어요.

 
어때요, 하나하나 보니까 생각보다 할 만하지요? 😀 이 밖에도 <circle>로 원 그리기, <text>로 글자 쓰기 등 많은 태그가 있지만, 위에 소개한 태그들만 알아도 SVG의 핵심은 대부분 배운 셈이에요. 이제 실제로 SVG를 어떻게 활용하는지 알아볼까요?
 

Figma에서 SVG 추출하기

다운받은 svg 파일들

 
Figma로 멋진 이미지를 만들었다고 가정해봐요. 그 이미지를 그대로 우리 React 앱에 가져오려면 SVG 파일로 추출하면 편해요. 방법은 정말 간단해요. Figma에서 원하는 객체를 선택하고 오른쪽 Export 패널에서 포맷을 SVG로 선택 후 Export 버튼을 눌러보세요.
 
또는 더 쉬운 방법으로, 객체를 오른쪽 클릭한 뒤 Copy as SVG를 선택하면 SVG 코드를 바로 복사할 수도 있어요 이렇게 얻은 SVG 코드나 파일을 이제 React에서 사용해보겠습니다.
 

React와 SVG로 세로 프로그레스 바 만들기

 
이제 Figma에서 가져온 SVG를 이용해, 세로로 된 프로그레스 바(슬라이더)를 만들어 볼 거예요.
이 프로그레스 바는 최소값(min)최대값(max) 두 가지 값을 가지며, 둘 다 사용자가 드래그해서 조절할 수 있는 인터랙티브한 컴포넌트예요. React와 TypeScript로 구현하면서, SVG로 도형을 그리고 pointer 이벤트로 드래그 기능까지 넣어볼게요!
 

1. 컴포넌트 상태 정의하기

 
먼저 React 함수 컴포넌트를 만들고, 프로그레스 바의 현재 최소값과 최대값을 상태(state)로 관리하겠습니다. React의 useState 훅을 사용해서 두 개의 숫자 상태 (minVal과 maxVal)를 만들어요. 초기값은 예를 들어 20과 80으로 설정할게요 (전체 범위를 0~100으로 생각). 그리고 드래그 중인 상태를 추적하기 위해 dragging이라는 상태도 하나 만들어요. dragging은 현재 드래그 중인 손잡이가 "min", "max" 또는 없음을 나타내도록 할 것입니다.

 
import React, { useState } from 'react';

function VerticalRangeBar() {
  const [minVal, setMinVal] = useState<number>(20);   // 최소값 (%)
  const [maxVal, setMaxVal] = useState<number>(80);   // 최대값 (%)
  const [activeThumb, setActiveThumb] = useState<'min' | 'max' | null>(null);
  ...
}

 
이렇게 상태를 정의해두면, 나중에 SVG에서 이 값들에 따라 손잡이 위치나 색깔 변화를 줄 수 있어요.

2. SVG로 모양 그리기

이제 컴포넌트의 JSX 반환값으로 SVG 요소를 작성해 볼게요.
우선 progress bar의 "몸통"을 그려볼게요.
 

      <defs>
        {/* Gradient & clip */}
        <linearGradient
          id="rangeGradient"
          gradientUnits="objectBoundingBox"
          x1="0"
          y1="0"
          x2="0"
          y2="1"
        >
          <stop offset="0%" stopColor="var(--ChartColor-ChartColor97, #FFFFE0)" />
          <stop offset="48.56%" stopColor="var(--ChartColor-ChartColor99, #89C0C4)" />
          <stop offset="100%" stopColor="var(--ChartColor-ChartColor100, #579EB9)" />
        </linearGradient>
        <clipPath id="rangeClip">
          <rect x={trackX} y={maxPos} width={trackThickness} height={minPos - maxPos} />
        </clipPath>

        {/* Button filter & clip */}
        <filter
          id="filter0_dd_1314_121150"
          x="0.186035"
          y="0.25"
          width="50"
          height="38"
          filterUnits="userSpaceOnUse"
          colorInterpolationFilters="sRGB"
        >
          <feFlood floodOpacity="0" result="BackgroundImageFix" />
          <feColorMatrix
            in="SourceAlpha"
            type="matrix"
            values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
            result="hardAlpha"
          />
          <feOffset />
          <feGaussianBlur stdDeviation="1.1" />
          <feColorMatrix
            type="matrix"
            values="0 0 0 0 0.0117647 0 0 0 0 0.0156863 0 0 0 0 0.0156863 0 0 0 0.56 0"
          />
          <feBlend
            mode="normal"
            in2="BackgroundImageFix"
            result="effect1_dropShadow_1314_121150"
          />
          <feColorMatrix
            in="SourceAlpha"
            type="matrix"
            values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
            result="hardAlpha"
          />
          <feOffset />
          <feGaussianBlur stdDeviation="0.6" />
          <feColorMatrix
            type="matrix"
            values="0 0 0 0 0.0117647 0 0 0 0 0.0156863 0 0 0 0 0.0156863 0 0 0 0.5 0"
          />
          <feBlend
            mode="normal"
            in2="effect1_dropShadow_1314_121150"
            result="effect2_dropShadow_1314_121150"
          />
          <feBlend
            mode="normal"
            in="SourceGraphic"
            in2="effect2_dropShadow_1314_121150"
            result="shape"
          />
        </filter>
        <clipPath id="clip0_1314_121150">
          <rect x="15.186" y="12.25" width="20" height="14" rx="5" fill="white" />
        </clipPath>
      </defs>

      {/* Track background */}
      <rect
        x={trackX}
        y={margin}
        width={trackThickness}
        height={height}
        fill="#ddd"
       
      />
      {/* Gradient clipped to range */}
      <rect
        x={trackX}
        y={margin}
        width={trackThickness}
        height={height}
        fill="url(#rangeGradient)"
        clipPath="url(#rangeClip)"
 
      />

 
 
위 코드에서 <linearGradient id="rangeGradient">를 정의하고, 트랙용 <rect>의 fill에 url(#"rangeGradient")을 지정했어요.
이렇게 하면 트랙 사각형에 gradient 효과를 지정할 수 있습니다.
def 안에 각종 효과(gradient, filter, clip path 등)을 미리 선언해놓고, 다른 컴포넌트에서 해당 효과를 id를 참조해서 사용이 가능합니다. 
 

track example

 
"track"을 그려놓고 지금 min, max 값만큼 clip해서 그라데이션 효과를 적용합니다.
 
이제 손잡이를 그려볼게요. 손잡이는 min과 max 값을 조절하는 버튼이라고 생각하면 됩니다. 손잡이는 아까 따로 다운로드해두었던 scroll.svg 를 활용합니다. 
 

...(위 코드 생략)

    <g
        transform={`translate(${btnOffsetX}, ${btnOffsetY(minPos)})`}
      >
        <SliderButton
          onMouseDown={e => { e.preventDefault(); setActiveThumb('min'); }}
        />
      </g>

      {/* Max thumb */}
      <g
        transform={`translate(${btnOffsetX}, ${btnOffsetY(maxPos)})`}
      >
        <SliderButton
          onMouseDown={e => { e.preventDefault(); setActiveThumb('max'); }}
        />
      </g>
    </svg>
    
interface SliderButtonProps {
  onMouseDown: (e: React.MouseEvent<SVGRectElement, MouseEvent>) => void;
}

const SliderButton: React.FC<SliderButtonProps> = ({ onMouseDown }) => (
  <g filter="url(#filter0_dd_1314_121150)">
    <path
      d="M12.186 17.25C12.186 14.4886 14.4246 12.25 17.186 12.25H33.186C35.9475 12.25 38.186 14.4886 38.186 17.25V21.25C38.186 24.0114 35.9475 26.25 33.186 26.25H17.186C14.4246 26.25 12.186 24.0114 12.186 21.25V17.25Z"
      fill="#F4F4F4"
    />
    <g clipPath="url(#clip0_1314_121150)">
      <rect
        x="15.186"
        y="12.25"
        width="20"
        height="14"
        rx="5"
        fill="#D9D9FF"
        fillOpacity="0.11"
      />
      <path
        d="M19.8527 23.25H30.5194C30.886 23.25 31.186 22.95 31.186 22.5833C31.186 22.2167 30.886 21.9167 30.5194 21.9167H19.8527C19.486 21.9167 19.186 22.2167 19.186 22.5833C19.186 22.95 19.486 23.25 19.8527 23.25ZM19.8527 19.9167H30.5194C30.886 19.9167 31.186 19.6167 31.186 19.25C31.186 18.8833 30.886 18.5833 30.5194 18.5833H19.8527C19.486 18.5833 19.186 18.8833 19.186 19.25C19.186 19.6167 19.486 19.9167 19.8527 19.9167ZM19.186 15.9167C19.186 16.2833 19.486 16.5833 19.8527 16.5833H30.5194C30.886 16.5833 31.186 16.2833 31.186 15.9167C31.186 15.55 30.886 15.25 30.5194 15.25H19.8527C19.486 15.25 19.186 15.55 19.186 15.9167Z"
        fill="#C6C6C6"
      />
    </g>
    {/* Transparent rect to capture events */}
    <rect
      width="51"
      height="39"
      fill="transparent"
      onMouseDown={onMouseDown}
    />
  </g>
);

 
버튼 좌표 배치 원리는 간단한데, transform 애니메이션을 활용해서 지금 "y 값 좌표" 에 버튼을 위치시킵니다. 
 

최종 프로그래스바 UI

 
 

3. 포인터 이벤트로 드래그 기능 추가하기

이제 가장 재미있는 부분입니다. 손잡이를 드래그해서 위아래로 움직이면 값이 변하도록 만들어볼 거예요.
 
웹에서 드래그 동작을 구현하려면 마우스나 터치 이벤트를 다뤄야 하는데, React에선 Pointer Events를 쓰면 아주 편리합니다. Pointer Event는 마우스, 터치, 펜 등 다양한 입력을 하나의 통일된 방식으로 처리할 수 있는 이벤트예요 예전에는 PC와 모바일을 모두 지원하려면 onMouseDown과 onTouchStart 등을 각각 코딩해야 했지만, 이제는 onPointerDown 하나로 모두 처리할 수 있답니다.
 
우리는 이미 JSX에서 <circle> 손잡이에 onPointerDown을 설정해 두었죠? 사용자가 손잡이를 누르는 순간 어떤 손잡이를 움직이는지 상태를 업데이트하고 (dragging을 "min" 또는 "max"로 set), 이후 움직이는 동안(pointermove)과 놓는 순간(pointerup)을 포착해서 값을 갱신하면 돼요.
 

  // Handles the pointer down event when a thumb is pressed
  // Sets the active thumb ('min' or 'max') and captures the pointer
  const handlePointerDown = (thumb: 'min' | 'max') => (e: React.PointerEvent) => {
     ...
  };
  
  // Handles the pointer move event when the thumb is dragged
  // Updates the value of the active thumb based on the pointer's position
  
  const handlePointerMove = (e: React.PointerEvent) => {
	...
  };
  
  // Handles the pointer up event when the thumb is released
  // Releases the pointer capture and clears the active thumb
  
  const handlePointerUp = (e: React.PointerEvent) => {
   	...
  };

 
 

최종 결과물

 
최종 코드는 아래에 있습니다.
 
https://codesandbox.io/p/devbox/kll755
 

결론: SVG 활용 꿀팁 ✨

이번 예제로 SVG의 힘을 조금 느끼셨나요? 정리하자면, SVG는 화면 크기에 관계없이 선명하고 코드로 그리는 그림이라 자유롭게 수정하거나 상호작용할 수 있다는 게 장점이에요. 마무리로, 실무에서 SVG를 유용하게 활용하는 몇 가지 팁을 알려드릴게요:
 

  • 반응형 디자인: SVG는 벡터 방식이라 화면 크기에 따라 자유롭게 확대/축소해도 깨지지 않아요. 따라서 아이콘이나 일러스트를 SVG로 사용하면 레티나 디스플레이에서도 언제나 또렷하게 보입니다. CSS로 너비나 높이를 %로 주거나 viewBox를 적절히 설정하면 부모 컨테이너 크기에 맞춰 유연하게 대응할 수도 있어요.
  • 아이콘 최적화: Figma나 일러스트레이터에서 SVG를 내보낼 때 불필요한 데이터가 붙거나 좌표가 복잡하게 저장될 수 있어요. 이런 경우 SVG 압축 도구를 사용해보세요. 예를 들어 SVGO라는 오픈 소스 툴을 쓰면 SVG 파일 크기를 많이 줄일 수 있어요복잡한 경로는 단순화하고, 쓰이지 않는 요소는 제거해서 성능을 높이는 거죠. 최적화된 SVG는 파일 크기가 작아져 웹페이지 로딩도 빨라진답니다.

  • 인터랙티브 컴포넌트: SVG 요소들은 DOM 요소이기도 해서, JavaScript나 CSS로 동적으로 제어하기가 쉬워요. 이번에 만든 프로그레스 바처럼 드래그 이벤트를 처리하는 것은 물론이고, <circle>에 마우스오버하면 색을 바꾸거나, 클릭하면 애니메이션을 주는 것도 가능합니다. 예를 들어, SVG에 CSS 클래스를 적용해서 .active일 때 특정 부분의 색깔을 변경하거나, 간단한 트랜지션 효과를 줄 수도 있어요. 또한 D3.js나 GSAP 같은 라이브러리와 결합하면 복잡한 데이터 시각화나 모션 그래픽도 구현할 수 있어요. 상상한 대로 자유롭게 SVG를 가지고 놀아보세요!

SVG를 배우기 시작하면 웹 개발에서 디자인과 상호작용을 다루는 새로운 무기가 생긴 셈이에요. 이번 튜토리얼이 따뜻한 입문 가이드가 되었길 바랍니다. 앞으로도 SVG를 활용해 멋지고 유용한 컴포넌트들을 많이 만들어보세요! 😃
 
P.S) 엄청 복잡한 인터렉션은 가내수공업이 아니고 lottieFiles같은 라이브러리의 힘을 빌리는게 좋을듯..?

반응형
반응형

 

canvas에서 충돌 애니메이션을 구현하는 방법을 간단하게 알아봅시다. 

아래 글을 이해하는데는 벡터 관련 지식이 약간 필요합니다.

출처: https://www.amazon.com/HTML5-Canvas-Native-Interactivity-Animation/dp/1449334989

 

 

핵심 개념은 단 세 가지뿐입니다.

  1. “좌표” 2. “속도(움직임)” 3. “거울에 비치듯 방향을 뒤집기”

1. 좌표 — 종이에 그린 그래프랑 똑같다

 

용어 뜻 예시

 

x 왼쪽 ↔ 오른쪽 거리 x = 0 → 맨 왼쪽, x = 500 → 맨 오른쪽
y 위 ↔ 아래 거리 y = 0 → 맨 위, y = 500 → 맨 아래

 

화면을 가로 500 × 세로 500 칸짜리 눈금종이라 생각하면 됩니다.

빨간 공 하나를 “좌표 (x, y)” 두 숫자로 위치시킵니다.

 

2. 속도 = “한 프레임(0.016초쯤)마다 몇 칸 움직이느냐”

let vx = 3;   // x축으로 한 번에 +3칸
let vy = 2;   // y축으로 한 번에 +2칸
  • 속도 벡터 (vx, vy) : “오른쪽으로 3칸, 아래로 2칸”이라는 화살표
  • 움직이기 : x += vx; y += vy;
  • → 숫자를 더하기만 하면 새 위치가 나옵니다.

👉 ‘벡터’란 단어를 어려워하지 마세요.

그냥 “Δx(+3), Δy(+2)”라는 두 개의 숫자를 한 쌍으로 들고 다닌다는 뜻뿐입니다.

 

3. 충돌 감지 — “부딪쳤다”를 어떻게 알까?

 

3‑1. 벽(사각형)과 부딪힘

  • 왼쪽 벽 : 공의 왼쪽 끝 x - R 이 0보다 작아졌다면 → “벽을 뚫었다”는 뜻
  • 오른쪽 벽 : x + R 이 500보다 크면 뚫음
  • 위·아래도 같은 논리로 구현
if (x - R <= 0 && vx < 0) vx = -vx;   // 왼쪽 벽
  • vx < 0 여야만 뒤집는 이유: 이미 오른쪽(+)으로 가고 있는 상황이라면 굳이 뒤집을 필요 없죠.

3-2. 동글동글 장애물(원)과 부딪힘

  • 두 원 중심 사이의 거리
  • 공 반지름 + 장애물 반지름 보다 작으면 겹쳤다(충돌)
const dx = x - obs.x;
const dy = y - obs.y;
const dist = Math.hypot(dx, dy);   // √(dx² + dy²)
if (dist < R + obs.r) { … }

 

** Math.hypot(a,b) 는 “피타고라스” 공식(√(a²+b²))을 간단히 써 주는 함수


4 튕겨 나가기 — 거울에 비친 화살표처럼 “반사”하기

 

4‑1. 벽은 간단 — 한 축만 부호 반전

  • 왼·오른쪽 벽 → x 방향만 뒤집기 → vx = ‑vx
  • 위·아래 벽 → y 방향만 뒤집기 → vy = ‑vy

결과 : “↘” 로 가던 화살표가 벽에 부딪히면 “↙”로 바뀝니다.

4‑2. 둥근 벽(원) — 법선(normal)이라는 “정면 방향”을 이용

  1. 법선 : 공→장애물로 그린 선을 1칸 길이로 만든 화살표
  2. → (nx, ny) = (dx/dist, dy/dist)
  • 정의: 법선 벡터는 “충돌 지점에서 표면에 수직으로 뻗은 단위 벡터”를 말해요.
  • 충돌 예제에서는 공→장애물 중심을 잇는 방향으로 벡터를 구한 뒤, 이걸 길이 1로 정규화(normalize) 한 것이 법선입니다.

법선 계산하는 방법

 

  1. 반사 공식
    • v·n (v dot n) = vx·nx + vy·ny
      스칼라곱: 그냥 두 숫자씩 곱해서 더한 값입니다

3. 코드

const dot = vx*nx + vy*ny;
vx = vx - 2 * dot * nx;
vy = vy - 2 * dot * ny;

왜 이렇게 하면 “거울 반사”가 될까?

화살표(속도)를 “정면 성분”과 “옆으로 스치는 성분”으로 쪼갠 뒤,

정면 성분만 → 반대 방향으로 두 배 돌려서 빼 버린다고 생각하면 됩니다.

한글 말장난보다, 거울에 비친 모습을 빼서 더한다는 직관이 더 쉬워요!

 

5 끼임 방지 — 살짝 밀어내기

충돌 직후, 공이 이미 장애물 안쪽에 일부 들어가 있을 수 있습니다.

겹친 거리(overlap)만큼 한 발짝 밀어내기

 

✋ 정리

궁금증 아주 쉬운 답

“벡터?” 숫자 두 개를 한 묶음(↗ 화살표)으로 본다
“dot(스칼라곱)?” a₁·b₁ + a₂·b₂ — 곱하고 더하기뿐
“왜 반사가 되죠?” 화살표를 거울에 비춘 뒤 그 방향으로 바꿔 꽂는 것
“무슨 고급 수학 쓰나요?” 피타고라스(√)와 곱셈·뺄셈 끝!

결국 우리가 한 일

① 위치(x, y)를 더하기로 옮겼다 → ② 부딪혔는지 간단한 비교·√로 확인 →

③ 맞으면 화살표(vx, vy)를 뒤집거나 반사 공식으로 바꿨다.

 

 

코드 예시

 

충돌 시뮬레이션 예시 코드(동영상은 20FPS)

 

 

아래 링크에서 확인 가능합니다.

 

https://codesandbox.io/p/devbox/kc8674

 

 

 

reference 

 

HTML5 Canvas: Native Interactivity and Animation for the Web 에서 일부 내용 발췌

반응형
반응형

요즘 애니메이션 구현해보는거에 재미들렸는데,

부드러운 애니메이션을 구현하는 방법에 대해 작성해볼려고 합니다.

 

서론

 

현대 웹과 앱에서는 작은 애니메이션 효과들이 사용자 경험을 크게 향상시킵니다. 눈에 잘 띄지 않는 마이크로 인터랙션(micro-interaction)이라도 인터페이스를 더욱 직관적이고 재미있게 만들어 주며, 사용자에게 피드백을 주어 참여도를 높입니다.

그런데, 애니메이션이 너무 갑작스럽거나 뚝뚝 끊긴다면 오히려 거슬릴 수 있습니다. 특히 모든 움직임이 선형(linear), 즉 처음부터 끝까지 일정한 속도로 진행되면 어딘가 모르게 어색하고 부자연스럽게 느껴지곤 합니다. 현실 세계를 생각해보면, 물체가 움직일 때 처음부터 끝까지 정확히 같은 속도로 움직이는 일은 드물죠.

 

예컨대 공을 던지면 처음에는 속도가 점점 빨라졌다가 나중에는 느려지면서 멈추고, 차가 출발하거나 정지할 때도 서서히 가속하고 감속합니다. 이런 부드러운 속도 변화를 애니메이션에 도입하면 움직임이 훨씬 자연스러워집니다.

 

이번 글에서는 이러한 이징(easing) 함수의 개념을 알아보고, JavaScript로 Canvas에서 부드러운 애니메이션을 구현하는 방법을 함께 살펴보겠습니다.

 

이징 함수란?

애니메이션에서 이징 함수는 움직임의 속도 변화를 결정하는 수학적 함수입니다. 쉽게 말해, 애니메이션이 진행되는 시간에 따른 속도의 변화율을 정의하는 함수라고 할 수 있습니다​.

이징 함수 f(t)는 일반적으로 t=0일 때 시작 값 0을 반환하고 t=1일 때 최종 값 1을 반환합니다. (즉, f(0) = 0이고 f(1) = 1입니다.) 다만 0 < t < 1인 중간 구간에서는 선형적인 증가 대신 특수한 곡선을 따라 값을 증가시키죠​. 이 함수를 통해 애니메이션 속도가 시간이 지남에 따라 어떻게 변할지 결정할 수 있습니다.

 

한마디로 정리하면, 이징 함수는 애니메이션의 진행률을 입력 받아 우리가 원하는 방식으로 조정한 새로운 진행률을 출력해주는 함수입니다. 이렇게 변형된 진행률을 이용하면 객체의 움직임이나 변화에 속도 완급을 줄 수 있습니다. 이징 함수를 사용하면 애니메이션이 한결 부드럽고 자연스러운 느낌을 갖게 되며, 사용자는 변화가 일어나는 방식을 쉽게 예측하고 받아들일 수 있습니다.

 

이징 함수 예제

 

이징 함수의 개념을 이해했으니, 구체적인 예제를 통해 어떤 종류의 이징 함수가 어떤 느낌의 애니메이션을 만드는지 알아보겠습니다. 흔히 쓰이는 이징 패턴에는 ease-in, ease-out, ease-in-out 등이 있는데, 이는 말 그대로 애니메이션의 시작이나 끝 부분의 움직임을 완만하게 만든다는 뜻입니다.

 

예를 들어:

  • ease-in: 처음엔 느리게 시작해서 나중에 빨라지는 패턴 (초반 가속)
  • ease-out: 처음엔 빠르게 시작해서 나중에 느려지는 패턴 (후반 감속)
  • ease-in-out: 처음과 끝은 느리고 중간은 가장 빠른 패턴 (초반 가속 + 후반 감속)
// 선형(linear) 함수: 변화 비율이 일정 (직선 그래프)
function linear(t: number): number {
  return t;
}

// Quadratic ease-in: 천천히 시작해서 가속 (f(t) = t^2)
function easeInQuad(t: number): number {
  return t * t;
}

// Quadratic ease-out: 빨리 시작해서 서서히 감속 (f(t) = 1 - (1-t)^2 와 동일)
function easeOutQuad(t: number): number {
  return 1 - (1 - t) * (1 - t);
}

// Quadratic ease-in-out: 초반과 후반은 완만, 중간은 빠름
function easeInOutQuad(t: number): number {
  return t < 0.5 
    ? 2 * t * t                             // 절반 이전에는 가속 (ease-in)
    : 1 - Math.pow(-2 * t + 2, 2) / 2;      // 절반 이후에는 감속 (ease-out)
}

 

이러한 이징을 적용하지 않은 기본 상태를 linear(선형)라고 부릅니다. Linear는 말 그대로 등속도로 움직이는 것으로, 어떤 순간에도 속도가 변하지 않습니다.

 

Canvas와 애니메이션 루프

 

브라우저에서 애니메이션을 구현할 때는 보통 반복적인 화면 갱신이 필요합니다. HTML5의 Canvas 요소는 자바스크립트를 통해 도형을 그리거나 지울 수 있게 해주는데, 여기에 애니메이션 루프를 적용하면 매 순간 화면을 업데이트하여 움직이는 효과를 만들 수 있습니다. 애니메이션 루프를 돌리는 대표적인 방법이 window.requestAnimationFrame() 함수입니다.

requestAnimationFrame은 브라우저에게 "다음 번 화면을 그리기 전에 이 함수를 실행해줘" 하고 요청하는 메서드입니다​.

한 번 호출하면 브라우저가 다음 리페인트(화면 재렌더링) 시점에 우리가 넘긴 콜백 함수를 호출해주며, 보통 초당 60회 정도 빈도로 불러줍니다. requestAnimationFrame을 이용하면 브라우저의 화면 재생 주기에 맞춰 그리기 때문에, setTimeout이나 setInterval로 임의로 루프를 돌리는 것보다 부드럽고 안정적인 애니메이션이 구현됩니다.

또한, 해당 탭이 비활성화된 경우 자동으로 중지하여 불필요한 연산을 피하는 최적화도 제공하지요.

 

사용법은 간단합니다. 애니메이션하고 싶은 내용을 처리하는 함수를 하나 만든 뒤, 그 함수 내부에서 매번 requestAnimationFrame을 재귀적으로 다시 호출해주면 됩니다. 이렇게 하면 함수가 반복해서 호출되며 루프가 만들어집니다. 이 때 현재 경과 시간을 이용해 애니메이션의 진행률(progress)을 계산하고, 그 진행률에 이징 함수를 적용해 객체의 상태를 업데이트하면 부드러운 속도 변화를 줄 수 있습니다. 아래는 간단한 애니메이션 루프의 예시 코드입니다.

 

const duration = 1000; // 애니메이션 지속 시간 (밀리초)
let startTime: number | undefined;

// 애니메이션 루프 함수
function frame(timestamp: number) {
  if (startTime === undefined) {
    startTime = timestamp;                   // 첫 프레임의 시작 시각 기록
  }
  const elapsed = timestamp - startTime;     // 경과 시간(ms)
  let progress = elapsed / duration;         // 0~1 사이의 진행률 계산
  if (progress > 1) progress = 1;            // 100%를 넘지 않도록 보정

  // 이징 함수를 적용하여 보정된 진행률 계산 (예: easeOutQuad 사용)
  const easedProgress = easeOutQuad(progress);

  // [여기에서 easedProgress에 따라 애니메이션 상태를 계산하고 그리기 작업 수행]
  // 예시: 어떤 요소의 X좌표를 0에서 100으로 이동한다면
  // let currentX = 0 + (100 - 0) * easedProgress;

  if (progress < 1) {
    requestAnimationFrame(frame);            // 아직 끝나지 않았으면 다음 프레임 예약
  }
}

// 애니메이션 시작
requestAnimationFrame(frame);

 

간단히 설명하자면, 지금 값 = 이전 값 + 변화량(Delta) 라고 보면 됩니다. 미분 적분할때 배웠던 변화량을 계속 더해나가면 되는데요, 

  if (startTime === undefined) {
    startTime = timestamp;                   // 첫 프레임의 시작 시각 기록
  }
  const elapsed = timestamp - startTime;     // 경과 시간(ms)

 

첫 프레임에서는 startTime이 비어 있으므로 초기값을 설정하고, 이후에는 elapsed를 통해 경과 시간을 계산합니다.

progress는 elapsed/duration으로 현재 전체 애니메이션에서 얼마만큼 진행됐는지를 0부터 1 사이로 나타낸 값입니다.

 

let progress = elapsed / duration;         // 0~1 사이의 진행률 계산
  if (progress > 1) progress = 1;            // 100%를 넘지 않도록 보정

  // 이징 함수를 적용하여 보정된 진행률 계산 (예: easeOutQuad 사용)
  const easedProgress = easeOutQuad(progress);

 

이 값을 이징 함수 easeOutQuad에 통과시키면 easedProgress를 얻게 됩니다. 이제 easedProgress를 가지고 애니메이션 대상의 현재 상태를 계산합니다.

 

위 예시 주석에서는 0에서 100 사이 이동하는 X좌표를 계산했는데, progress 대신 easedProgress를 사용함으로써 처음에는 빠르게 움직이고 끝에 갈수록 천천히 움직이게 됩니다.

 

마지막으로 진행률이 1(100%)가 아닐 경우 requestAnimationFrame(frame)을 다시 호출하여 다음 프레임을 예약합니다. 이렇게 함수를 자기 자신으로 재호출함으로써 약속된 시간동안 루프가 돌며 애니메이션이 이어집니다.

 

 

부드러운 인터렉션 예시 (비디오 프레임이 낮은데 실제로 보면 더 자연스럽습니다..ㅋㅋ )

 

위 progress bar 코드 예시는 아래 링크에 있습니다.

https://codesandbox.io/p/devbox/yqt69q?embed=1

 

브라우저 easing 함수

브라우저에서는 css에서 이런 이징 함수들을 쉽게 사용 가능하도록 여러가지 옵션을 부여합니다,

아래는 대표적인 CSS 이징 함수들의 개념과 사용 예시입니다. 각 함수를 transition-timing-function 또는 animation-timing-function에 지정해 애니메이션 속도를 조절할 수 있습니다.

/* ease: 시작과 끝에서 느리게, 중간에 빠르게 진행되는 기본 이징 */
animation-timing-function: ease;

/* ease-in: 시작 부분에서 느리게 출발해 점진적으로 가속 */
animation-timing-function: ease-in;

/* ease-out: 처음에는 빠르게 진행하다가 끝 부분에서 느리게 감속 */
animation-timing-function: ease-out;

/* ease-in-out: 시작과 끝은 느리게, 중간 구간은 빠르게 진행 */
animation-timing-function: ease-in-out;

/* linear: 시작부터 끝까지 일정한 속도로 균일하게 진행 */
animation-timing-function: linear;

/* step-start: 즉시 첫 번째 단계로 점프하고, 이후 단계 전환 없이 유지 */
animation-timing-function: step-start;

/* step-end: 마지막 단계 전환은 끝 부분에 이루어지고, 그 전까지는 이전 상태 유지 */
animation-timing-function: step-end;

 

마무리

이 글에서는 이징 함수의 개념과 활용법을 살펴보고, 간단한 애니메이션 예제로 적용해보았습니다. 정리하자면, 이징 함수를 사용하면 애니메이션에서 속도의 완급 조절이 가능해지고, 이를 통해 사용자에게 부드럽고 자연스러운 인터랙션을 제공할 수 있습니다. 선형으로 움직이는 것보다 살짝 가속도나 감속도가 들어간 움직임이 훨씬 현실적이고 매끄럽게 느껴지는 것이죠

 

특히 UI 요소의 등장/퇴장이나 강조 효과 등에 이징을 잘 사용하면, 인터페이스가 한층 세련되고 친절한 느낌을 주게 됩니다.

 

여기서 다룬 Quadratic, Cubic 등의 이징 함수 외에도 매우 다양한 이징 함수들이 존재합니다. 예를 들어 탄성(elastic) 효과로 물체가 용수철처럼 튕기는 움직임이나, 바운스(bounce) 효과로 공이 바닥에 떨어졌다 튕기는 움직임 등도 구현할 수 있습니다​

 

이런 특수한 이징 함수들은 조금 더 복잡한 수식을 사용하지만 원리는 같습니다. 

 

마지막으로, 이징 함수를 잘 활용하는 팁은 어떤 상황에 어떤 느낌을 주고 싶은지 생각하는 것입니다. 부드럽게 시작해야 할까요, 아니면 부드럽게 멈춰야 할까요, 혹은 둘 다일까요? 그런 의도에 따라 적절한 함수를 골라 적용해보고, 눈으로 결과를 확인하면서 감을 익혀보세요.

 

reference 

https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function

 

반응형

+ Recent posts