본문 바로가기

웹 기초

[Vanila JavaScript] 파티클 만들기

오늘은 내일 배움 캠프 자기소개서 페이지 제작 중, 적용한 파티클 만드는 방법에 대해서 리뷰를 하고자 한다.

 

<script>
  const canvas = document.getElementById('particlesCanvas');
  const ctx = canvas.getContext('2d');
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  let animationStarted = false;	// 애니메이션 시작 확인 변수

  let particles = [];	// 파티클 담는 배열 (애니메이션 중 움직이는 여러 개의 파티클 객체들이 저장)
  ..
  • id가 particlesCanvas인 <canvas> 요소를 선택해서 저장한다.
  • canvas 메서드인 getContext('2d') : <canvas> 요소에서 2D 그래픽을 그릴 수 있는 컨텍스트(context)를 반환한다.
  • canvas 너비를 현재 브라우저 창의 너비(window.innerwidth)로 설정한다.
  • canvas 높이를 현재 브라우저 창의 높이(window.innerheight)로 설정한다.

 

 

  function createParticles(x, y) {	// x, y 위치를 기준으로 입자(particle)들을 생성하는 함수
    for (let i = 0; i < 30; i++) {
      particles.push({
        x,
        y,
        radius: Math.random() * 20 + 10,
        emoji: '❤️',
        speedX: (Math.random() - 0.5) * 4,
        speedY: (Math.random() - 0.5) * 4,
        life: 100
      });
    }
  }
  • 반복문 30번 반복 : 30개의 파티클을 생성하기 위한 반복문이다.
  • particles.push({}) : 파티클을 저장하는 배열에 객체({key:value} 형태) 를 추가하였다. 
  • 파티클 객체는 다음과 같은 정보를 같는다. 
    1. x(x : x 와 동일하다. key와 value 값이 동일할 때 x로 줄여서 사용.) → 파티클 시작 위치 중 x 값 저장
    2. y(위와 동일한 이유) → 파티클 시작 위치 중 y 값 저장
    3. radius : Math.radnom() * 20 + 10 → 파티클의 크기를 설정. (10~30 사이의 값이 랜덤으로 결정)
    4. emoji : '❤️' → 입자에 표시될 이모지이다.
    5. speedX, Y : (Math.random() - 0.5) * 4  → 파티클이 움직이는 속도. (-2~2의 속도를 갖는다. = 상하좌우 랜덤한 방향으로 퍼진다.)
    6. life: 100 → 파티클의 수명. (수명을 줄여 입자가 점점 사라지게 관리할 수 있다.)

 

 

 

  let lastScrollY = window.scrollY;

  function updateParticles() {	// 입자들을 매 프레임마다 업데이트하고 다시 그리는 함수
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    particles = particles.filter(p => p.life > 0);

    const scrollOffsetY = window.scrollY - lastScrollY;

    particles.forEach(p => {
      // 위치 업데이트
      p.x -= p.speedX;
      p.y -= p.speedY + scrollOffsetY;  // 스크롤 보정!

      p.life--;

      ctx.font = `${p.radius}px serif`;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(p.emoji, p.x, p.y);
    });

    lastScrollY = window.scrollY;
    requestAnimationFrame(updateParticles);
  }
  • let lastScrollY = window.scrollY → 이전 프레임의 스크롤 위치를 저장한다.
  • ctx.clearRect(0, 0, canvas.width, canvas.height) → 이전 프레임에 그려진 그래픽을 모두 지운다.
  • particles = particles.filter(p => p.life > 0) → life가 0보다 큰 입자만 남긴다. 수명이 다된 입자들은 배열에서 제거
  • const scrollOffsetY = window.scrollY - lastScrollY → 현재 스크롤 위치와 이전 스크롤 위치의 차이를 구한다.
  • particles.forEach(p => { → 남아 있는 모든 입자들에 대해 하나씩 처리한다. (비용이 만만치 않을 예정이라는 것이다.)
    1. p.x -= p.speedX → 입자의 x 좌표를 속도만큼 이동시킨다. 왼쪽 또는 오른쪽으로 움직인다.
    2. p.y -= p.speedY + scrollOffsetY; → y좌표도 속도만큼 이동시키고, 여기에 스크롤 변화량도 더한다. (스크롤 시 보정을 하지 않으면, 파티클이 버튼을 눌렀던 위치가 아니라 스크롤된 화면 위치에 따라 움직이게 된다.)
    3. p.life-- → 입자의 수명을 하나 줄인다.
    4. ctx.font = `${p.radius}px serif → 이모지를 그릴 글꼴 크기를 입자의 radius 값으로 설정 (serif는 글꼴 종류인데, 이모지는 이 글꼴에서 잘 작동한다.)
    5. ctx.textAlign = 'center' → 이모지가 입자의 중심에 위치하도록 정렬 기준을 설정
    6. ctx.textBaseline = 'middle' → 이모지가 입자의 중심에 위치하도록 정렬 기준을 설정
    7. ctx.fillText(p.emoji, p.x, p.y) → 각 입자에 지정된 emoji 를 (p.x, p.y) 위치에 랜더링한다.
  • lastScrollY = window.scrollY → 현재 스크롤 위치를 저장하여 다음 프레임에서 스크롤 차이를 계산할 수 있도록 한다.
  • requestAnimationFrame(updateParticles) → 비동기적 재귀루프를 통해, 본인을 계속 호출하여 애니메이션 처럼 보이게 한다.

 

  document.querySelectorAll('.imgs button').forEach(button => {
  button.addEventListener('click', (e) => {
    const rect = e.target.getBoundingClientRect();
    const x = rect.left + rect.width / 2;
    const y = rect.top + rect.height / 2;
    createParticles(x, y);

    if (!animationStarted) {
      updateParticles();
      animationStarted = true;
    }
   });
  });

 

 

  window.addEventListener('resize', () => {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
  });
</script>

 

 

 

결과

다음과 같이 버튼을 누르면 사방으로 재각기 하트들이 파티클처럼 퍼지는 것을 볼 수 있다.