폭죽 애니메이션

프로젝트 소개

HTMLCanvas를 통해 폭죽이 터지는 모습과 마우스 커서의 움직임에 따른 상호작용 애니메이션을 구현해보았습니다.

언어

  • HTML
  • CSS
  • TypeScript

사용 기술

  • HTML Canvas

구현

폭죽

사용자의 화면이 로딩되게 되면 일정 주기 마다 폭죽이 하늘로 올라가고 시간이 지나면 마지막 위치에서 폭발하게 되면서 불꽃을 흩뿌리게 됩니다.

폭죽은 ExplodeSpark라는 클래스로 구현하였습니다.

Explode 클래스는 여러개의 Spark 클래스를 가지게 됩니다.

(폭죽의 폭발 안에 여러 불꽃들이 나타나게 되는거니 이런 방식으로 관리하게 되었습니다.)

// Explode
export class Explode {
  constructor(x, y, size) {
    this.x = x;
    this.y = y;

    this.sparks = [];
    for (let i = 0; i < size; i++) {
      this.sparks.push(new Spark(x, y));
    }
  }

  draw(ctx, stageWidth, stageHeight) {
    this.sparks.forEach((spark, i) => {
      if (spark.y > stageHeight) this.sparks.splice(i, 1);

      spark.animate(stageWidth);
      spark.draw(ctx);
    });
  }
}

Spark 클래스는 랜덤한 색상과 속도를 가지고 화면에 나타나게 됩니다.

// Spark
class Spark {
  constructor(x, y) {
    this.velocity = { x: negativeRandom(.2, 6), y: negativeRandom(.5, 8) };

    this.x = x;
    this.y = y;

    this.colors = ['#F263C0', '#F2AEE0', '#303473', '#F2BA52', '#F28D35'];
    this.color = this.colors[Math.round(Math.random() * this.colors.length - 1)];

    this.lastPoint = { x: this.x, y: this.y };
  }

  windowBounce(stageWidth) {
    if (this.x < 0 || this.x > stageWidth) {
      this.velocity.x *= -0.5;
      this.x += this.velocity.x;
    }
  }

  animate(stageWidth, stageHeight) {
    this.lastPoint.x = this.x;
    this.lastPoint.y = this.y;

    this.x += this.velocity.x;
    this.y += this.velocity.y;

    this.windowBounce(stageWidth, stageHeight);

    this.velocity.y += .08;
  }

  draw(ctx) {
    ctx.beginPath();
    ctx.moveTo(this.lastPoint.x, this.lastPoint.y);
    ctx.lineTo(this.x, this.y);
    ctx.closePath();
    ctx.lineWidth = 1;
    ctx.strokeStyle = this.color;
    ctx.stroke();
  }
}

또한 Spark는 브라우저 안에 갇혀있다는 느낌을 주고 싶어 화면의 가장자리에 부딪히면 틩겨나가는 애니메이션을 구현하였습니다.

구현은 아래와 같은 방식으로 하였습니다.

windowBounce(stageWidth) {
  if (this.x < 0 || this.x > stageWidth) {
    this.velocity.x *= -0.5;
    this.x += this.velocity.x;
  }
}

버튼

화면 중앙에 위치한 버튼을 클릭하면 주기적으로 나오는 폭죽 외에 추가적인 폭죽이 나타나 터지도록 구현해두었습니다.

그 외에 밋밋한 버튼의 디자인을 변경하고 싶어 svg를 이용해 다음과 같은 효과를 구현해보았습니다.

<svg>
  <defs>
    <filter id="wave">
      <feTurbulence type="fractalNoise" baseFrequency=".00001 .00001" numOctaves="1" result="warp"/>
      <feDisplacementMap xChannelSelector="R" yChannelSelector="G" scale="30" in="SourceGraphic" in2="warpOffset"/>
    </filter>
  </defs>
</svg>
svg {
  display: none;
}

button {
  cursor: pointer;

  position: absolute;
  top: 60%;
  left: 50%;

  width: 150px;
  height: 60px;

  color: #000;
  font-size: 1.5rem;
  font-weight: bold;

  border: none;
  background: none;
  outline: none;

  transform: translate(-50%, -50%);

  @media #{$mobile} {
    width: 100px;
    height: 45px;

    font-size: 1rem;
  }

  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;

    width: 100%;
    height: 100%;

    background-color: #FFF;

    filter: url('#wave');

    z-index: -1;
  }

  &:hover::before {
    animation: squish .5s;
  }
}
@keyframes squish {
  0% { transform: scale(1) }
  20% { transform: scale(.9) }
  60% { transform: scale(1.1) }
  100% { transform: scale(1) }
}

마우스 이펙트

마지막으로 추가한 기능은 마우스 움직임에 따른 이펙트입니다.

해당 이펙트는 마우스가 지나간 자리에 사방으로 움직이는 점들을 생성하게 되고 각각의 점들은 서로 상호작용하여 근접한 점들과 선으로 이어지게 됩니다.

각각의 점들은 다른 선들과의 거리를 계산하여 일정 거리안에 들어있다면 선을 생성하게 됩니다.

// particles line
const distance = 100;
for (let i = 0; i < this.particles.length; i++) {
  const cur = this.particles[i];

  for (let j = i + 1; j < this.particles.length; j++) {
    const tar = this.particles[j];

    if (getDistance(cur.x, cur.y, tar.x, tar.y) < distance) {
      this.ctx.beginPath();
      this.ctx.moveTo(cur.x, cur.y);
      this.ctx.lineTo(tar.x, tar.y);
      this.ctx.closePath();
      this.ctx.lineWidth = 1;
      this.ctx.strokeStyle = '#FFF';
      this.ctx.stroke();
    }
  }
}

마치며

다양한 기능을 넣어보고자 하다보니 컨셉을 벗어난 느낌은 들지만 그 과정에서 많은 것을 배울 수 있었습니다.