
KNUD 2026 졸업전시 웹사이트의 메인 화면에는 마우스 클릭과 이동에 반응하는 스프레이 인터랙션을 적용하고 있다. 목표는 단순히 점을 늘어놓는 효과가 아니라, 스프레이 캔을 분사했을 때처럼 밀도·번짐·색의 누적이 느껴지는 질감을 만드는 것이다.
이 효과는 정적인 이미지로도 만들 수 있지만, 사용자 입력에 따라 즉시 달라져야 한다. 따라서 결과는 래스터 질감을 띠되, 입자의 위치와 수명은 코드로 제어하는 방식이 적합했다.
아래 내용은 현재 구현을 설명하기 위한 학습·설계 기록이다. 성능 수치는 아직 동일 조건의 측정을 완료하지 않았으므로 결과로 단정하지 않는다.
래스터 이미지는 픽셀의 집합으로 구성된다. 사진이나 스프레이처럼 미세한 색 변화와 질감이 중요한 표현에 잘 맞지만, 원본보다 크게 확대하면 픽셀이 드러날 수 있다.
벡터 이미지는 점·선·곡선·도형을 수학적 정보로 저장한다. 크기를 바꿔도 선명도가 유지되고 수정이 쉽기 때문에 로고·아이콘·단순한 일러스트에 적합하다.
스프레이 인터랙션은 두 특성을 함께 가진다. 화면에 남는 결과는 많은 반투명 점이 겹친 래스터 질감이지만, 입자의 중심점·반경·투명도·생존 시간은 벡터적인 규칙으로 설계한다.
| 구분 | 래스터 | 벡터 | 스프레이 구현에서의 역할 |
|---|---|---|---|
| 구성 단위 | 픽셀 | 점·선·곡선·도형 | 작은 입자의 누적 |
| 확대 | 품질 저하 가능 | 선명도 유지 | 화면 배율과 DPR 대응 필요 |
| 강점 | 질감·색 변화 | 규칙·변형 | 질감과 제어를 함께 확보 |
| 예시 | 사진, 텍스처 | 로고, SVG | 입자 표현과 분사 규칙 |
스프레이처럼 짧은 시간 안에 많은 입자를 표시할 때, 입자 하나마다 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 좌표계는 그대로 유지하면서 렌더링 해상도만 높인다. 다만 픽셀 버퍼가 커질수록 그려야 할 면적도 늘어나므로, 무조건 높은 해상도를 쓰기보다 입자 예산과 함께 판단해야 한다.
스프레이의 품질은 입자 수에 비례하지만 성능도 함께 악화될 수 있다. 현재의 핵심 가설은 모든 포인터 이벤트를 입자로 변환하는 것이 아니라, 분사 간격·최대 입자 수·수명 제거를 조합하면 밀도감을 유지하면서 비용을 제한할 수 있다는 것이다.
실제 병목은 Chrome Performance 패널에서 CPU 스크립팅 시간, Paint·Composite 비용, 프레임 드롭, 메모리 증가를 같은 시나리오로 비교해 확인할 예정이다. 이 진단 시나리오는 프로젝트 Discussion에 별도로 기록해두었다.