스프레이 표현을 위한 래스터·벡터와 Canvas API 기초

이준섭·2026년 8월 28일

KNUD

목록 보기
1/6
post-thumbnail

배경

KNUD 2026 졸업전시 웹사이트의 메인 화면에는 마우스 클릭과 이동에 반응하는 스프레이 인터랙션을 적용하고 있다. 목표는 단순히 점을 늘어놓는 효과가 아니라, 스프레이 캔을 분사했을 때처럼 밀도·번짐·색의 누적이 느껴지는 질감을 만드는 것이다.

이 효과는 정적인 이미지로도 만들 수 있지만, 사용자 입력에 따라 즉시 달라져야 한다. 따라서 결과는 래스터 질감을 띠되, 입자의 위치와 수명은 코드로 제어하는 방식이 적합했다.

아래 내용은 현재 구현을 설명하기 위한 학습·설계 기록이다. 성능 수치는 아직 동일 조건의 측정을 완료하지 않았으므로 결과로 단정하지 않는다.

래스터와 벡터: 결과와 생성 규칙을 분리해서 보기

래스터 이미지는 픽셀의 집합으로 구성된다. 사진이나 스프레이처럼 미세한 색 변화와 질감이 중요한 표현에 잘 맞지만, 원본보다 크게 확대하면 픽셀이 드러날 수 있다.

벡터 이미지는 점·선·곡선·도형을 수학적 정보로 저장한다. 크기를 바꿔도 선명도가 유지되고 수정이 쉽기 때문에 로고·아이콘·단순한 일러스트에 적합하다.

스프레이 인터랙션은 두 특성을 함께 가진다. 화면에 남는 결과는 많은 반투명 점이 겹친 래스터 질감이지만, 입자의 중심점·반경·투명도·생존 시간은 벡터적인 규칙으로 설계한다.

구분래스터벡터스프레이 구현에서의 역할
구성 단위픽셀점·선·곡선·도형작은 입자의 누적
확대품질 저하 가능선명도 유지화면 배율과 DPR 대응 필요
강점질감·색 변화규칙·변형질감과 제어를 함께 확보
예시사진, 텍스처로고, SVG입자 표현과 분사 규칙

Canvas API를 선택한 이유

스프레이처럼 짧은 시간 안에 많은 입자를 표시할 때, 입자 하나마다 DOM 요소를 만들면 요소 수와 스타일 계산 비용이 계속 늘어날 수 있다. 특히 이동 중 매번 위치·투명도·크기를 갱신하면 레이아웃과 페인트 부담까지 함께 커질 수 있다.

Canvas API는 하나의 canvas에 직접 그리기 때문에 많은 입자를 반복적으로 표시하는 실험에 적합하다. Canvas는 화면의 이전 상태를 자동으로 관리하지 않는다. 애니메이션에서는 매 프레임 필요한 장면을 다시 그리는 구조를 명확히 설계해야 한다.

포인터 입력
  → 분사 간격 판정
  → 입자 데이터 생성
  → requestAnimationFrame
  → 오래된 입자 제거
  → canvas를 지우고 현재 입자만 다시 렌더링

최소 입자 모델

입자를 화면에 그린 결과가 아니라 데이터로 먼저 관리하면, 색상 전환·수명·밀도 제한을 서로 독립적으로 조절할 수 있다.

type Particle = {
  x: number;
  y: number;
  radius: number;
  color: string;
  alpha: number;
  bornAt: number;
  lifetime: number;
};

function createParticle(x: number, y: number, color: string, now: number): Particle {
  return {
    x: x + (Math.random() - 0.5) * 30,
    y: y + (Math.random() - 0.5) * 30,
    radius: 2 + Math.random() * 6,
    color,
    alpha: 0.15 + Math.random() * 0.35,
    bornAt: now,
    lifetime: 1800 + Math.random() * 1200,
  };
}

여기서 중요한 점은 이동 경로의 길이로 꼬리를 강제로 제한하지 않는 것이다. 대신 각 입자가 자신의 수명을 가지고 사라지게 하면, 계속 분사되는 느낌은 유지하면서도 오래된 점이 무한히 쌓이는 문제를 막을 수 있다.

자연스러운 스프레이를 만드는 변수

단순한 무작위 배치만으로는 스프레이처럼 보이지 않는다. 다음 값은 질감과 성능 모두에 영향을 준다.

  • 입자 수와 분사 간격: 포인터 이벤트마다 생성하지 않고, 이동 거리 또는 시간 기준의 간격을 둔다.
  • 중심에서의 거리 분포: 중심 근처는 조밀하게, 바깥쪽은 성기게 배치해 분사 느낌을 만든다.
  • 입자 크기와 투명도: 모두 같은 값이면 도장처럼 보이므로 작은 범위 안에서 변화를 준다.
  • 색상 전환 규칙: 클릭마다 팔레트를 전환하되, 기존 입자는 생성 당시 색을 유지한다.
  • 수명: 오래된 입자는 점진적으로 투명해진 뒤 제거한다. 시각적 잔상과 메모리 상한을 함께 관리할 수 있다.

고해상도 화면에서의 선명도

CSS 크기와 실제 캔버스 픽셀 크기가 같으면 고밀도 화면에서 선이 흐리게 보일 수 있다. devicePixelRatio를 반영해 내부 버퍼를 늘리고, 그에 맞춰 좌표계를 스케일링해야 한다.

const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();

canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);

const context = canvas.getContext('2d');
context?.setTransform(dpr, 0, 0, dpr, 0, 0);

이 방식은 CSS 좌표계는 그대로 유지하면서 렌더링 해상도만 높인다. 다만 픽셀 버퍼가 커질수록 그려야 할 면적도 늘어나므로, 무조건 높은 해상도를 쓰기보다 입자 예산과 함께 판단해야 한다.

성능을 위한 가드레일

스프레이의 품질은 입자 수에 비례하지만 성능도 함께 악화될 수 있다. 현재의 핵심 가설은 모든 포인터 이벤트를 입자로 변환하는 것이 아니라, 분사 간격·최대 입자 수·수명 제거를 조합하면 밀도감을 유지하면서 비용을 제한할 수 있다는 것이다.

  • pointermove마다 생성하지 않고 거리·시간 임계값을 둔다.
  • 렌더링은 requestAnimationFrame에서만 수행한다.
  • 최대 입자 수를 두고, 한도를 넘으면 가장 오래된 입자부터 제거한다.
  • 화면 밖으로 나간 입자와 수명이 끝난 입자는 즉시 데이터 배열에서 제거한다.
  • 저사양 환경 또는 prefers-reduced-motion에서는 분사량과 갱신 빈도를 낮추는 대안을 둔다.

실제 병목은 Chrome Performance 패널에서 CPU 스크립팅 시간, Paint·Composite 비용, 프레임 드롭, 메모리 증가를 같은 시나리오로 비교해 확인할 예정이다. 이 진단 시나리오는 프로젝트 Discussion에 별도로 기록해두었다.

다음 과제

  1. 일정한 클릭·드래그 시나리오로 입자 수와 프레임 시간을 측정한다.
  2. 캔버스 크기와 devicePixelRatio 조합별 선명도·비용을 비교한다.
  3. 모바일 터치 입력에서도 같은 분사 간격 규칙이 자연스러운지 검토한다.
  4. 사용자 설정 또는 접근성 환경에서 인터랙션을 줄일 수 있는 옵션을 설계한다.

References

profile
글은 어떻게 잘 쓰는 걸까...

0개의 댓글