유성 배경효과를 만들어 보기

Wooki·2023년 12월 13일
0

CSS

목록 보기
9/9

유성처럼 별이 떨어지는 배경 화면을 적용해 봤다.

글 쓰다가 날아가서 다시.. 쓰는......

별 구현하기

일단 유성이 떨어지려면 별부터 만들어야 했다. 내가 생각한 방식은 div 같은 요소에 border-radius를 줘서 작은 원 형태로 만들고, box-shadow를 적용하여 별의 반짝임을 표현하고자 했다.

.star {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background-color: white;
  box-shadow: 0px 0px 10px #c77eff;
}

image

하지만 요소의 크기가 작아서 shadow가 거의 티가 나지 않았다. 이를 해결하려고 box-shadow에 대해서 더 찾아보던 중
mdn을 읽다가 box-shadow에 spread-radius라는 값을 줄 수 있다는 사실을 알게 되었다.

spread-redius는 그림자의 확산 범위를 지정해 줄 수 있는 값이다.

이를 이용해서 그림자의 확산 범위를 좁게 하여 그림자의 밝기를 올려줬다.

image

유성의 꼬리 구현하기

유성의 꼬리는 가상 선택자를 이용해서 구현했다. 유성의 꼬리는 보통 별에서 멀어질 수록 점점 옅어져야 하기 때문에
linear-gradient 속성을 주어서 점점 멀어지게 구현했다.

.star::after {
  position: absolute;
  top: calc(50% - 1px);
  left: -950%;
  width: 2000%; /* star의 크기에 따라서 꼬리의 길이도 다르게 해주고 싶어서 부모 요소의 길이에 비례하게 값 전달*/
  height: 2px;
  background: linear-gradient(to left, #fff0, #eee); /*서서히 흐려지는 꼬리*/
  content: "";
  transform: rotateZ(-45deg) translateX(50%);
}

그리고 유성이 대각선으로 떨어지는 효과를 주기 위해서 rotateZ값을 이용해 회전시켜 주고 회전한 만큼 translateX 값으로 위치를 조정했다.

image

유성이 떨어지는 효과 구현하기

유성이 떨어지는 효과는 animation을 이용해서 top값을 증가시켜서 떨어지게 만들고, translateX를 이용해서 대각선으로 흐르게 하면 되겠다 생각했다.

@keyframes meteor {
  0% {
    top: -10vh;
    transform: translateX(0px);
    opacity: 1;
  }
  100% {
    top: 110vh;
    transform: translateX(-120vh);
    opacity: 1;
  }
}
/* 처음에는 translateX가 아니라 left를 이용해서 키프레임을 적용했는데 이렇게 하니 시작 위치를 랜덤하게 잡는데 인라인으로 작성하기 불편해서 수정했다.*/
` .star {
  opacity: 0;
  animation: meteor 1s 1s infinite;
}

이 때 top이 이동하는 거리와 left가 이동하는 거리의 비율과 유성 꼬리의 각도가 중요하다. 삼각형의 탄젠트 값을 이용해서 tan (꼬리각도)˚ 의 값과 top,left의 이동 거리의 비가 같아야 한다.
지금 꼬리의 각도는 45도 이기 때문에 tan 45˚ = 1 이고, top의 이동거리와 left의 이동 거리의 비를 1로 맞췄다.

chrome-capture-2023-12-13

랜덤하게 떨어지기

하지만 이대로 별들의 개수를 늘려 봤자 별들이 같은 위치에서 같은 속도로 떨어지기 때문에 자연스럽지 못하다.

그래서 별마다 left값, animation의 delay와 duration을 랜덤하게 줘서 별들이 자연스럽게 제각각으로 떨어지는 효과를 주었다.

export default function MeteorEffect({ count = 6 }: MeteorEffectProps) {
  const starCount = count < MAX_STAR_COUNT ? count : MAX_STAR_COUNT;
  const [starInterval, setStarInterval] = useState < number > 0; // 별들의 최소 간격

  //
  useEffect(() => {
    const calcStarInterval = () => {
      let innerWidth = window.innerWidth;
      setStarInterval(Math.floor((innerWidth * 1.5) / (count * 5))); // window.innerWidth값을 별의 개수 의 배수로 나눠서 최소 간격을 지정
      // 화면 밖에서 떨어지기 시작하는 별을 위해서 innerWidth에 1.5배를 해줬다.
    };
    calcStarInterval();
    window.addEventListener("resize", calcStarInterval); // resize로 innerWidth값이 변경 되었을 때 별 간격을 다시 계산하기 위한 resize 이벤트 핸들러
    return () => {
      window.removeEventListener("resize", calcStarInterval);
    };
  }, []);

  return (
    <MeteorEffectLayout>
      {new Array(starCount).fill(0).map((e, idx) => {
        const left = `${Math.random() * count * 5 * starInterval}px`;
        const animationDelay = `${Math.random() * 15}s`;
        const animationDuration = `${2 + Math.random() * 4}s`;
        return (
          <div
            key={idx}
            style={{ left, animationDelay, animationDuration }}
            className="star"
          ></div>
        );
      })}
    </MeteorEffectLayout>
  );
}

각각 별들에 inline style로 left, delay, duration을 전달했다

  • left : window의 innerWidth를 count의 배수로 나눈 값을 별 사이의 간격으로 하고 별사이의 간격 * 랜덤 값 을 이용해서 시작 값을 랜덤하게 지정.
  • delay : left처럼 Math.random을 이용해서 0 ~ 최대 딜레이 시간 사이로 지정
  • duration : Math.randowm을 이용해서 최소 시간과 최대 시간을 지정해서 전달

이제 랜덤하게 별들이 떨어진다!

chrome-capture-2023-12-132

추가

이후에 인라인 스타일로 색상과 크기를 랜덤하게, 별이 떨어지는 방향과 별의 각도, 속도, 딜레이는 매개변수로 받아서 적용할 수 있도록 수정하였다.

  • angle(별똥별 각도) 에 따라서 rotateZ값을 수정하고, tan 각도 를 계산헤서 키프레임의 translate값을 조절하였다..
  • left , right 방향을 받아서 키프레임 translate 방향을 조절하였다.

chrome-capture-2023-12-123333

전체 코드

import React, { useEffect, useLayoutEffect, useState } from 'react'
import styled, { keyframes } from 'styled-components'

const MeteorKeyframe = (direction : "left" | "right", angle : number) => keyframes`
    0% {
        top: -10vh;
        transform: translateX(0px);
        opacity: 1;
    }
    100% {
        top: 110vh;
        transform: translateX(${direction ==="left" ? "-" : "+"}${120 / Math.tan((angle * Math.PI)/ 180)}vh);
        opacity: 1;
    }
`

interface MeteorLayoutProps {
    $direction: "left" | "right";
    $angle: number;
}

const MeteorEffectLayout = styled.div<MeteorLayoutProps>`
    position: absolute;
    top:0;
    left:0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background-color: black;
    .star {
        position: relative;
        top: 50%;
        width: 4px;
        height: 4px;
        border-radius: 50%;
        background-color: white;
        animation: ${(props) => MeteorKeyframe(props.$direction,props.$angle)} 4s ease-in infinite;
        opacity: 0;
    }
    .star::after {
        position: absolute;
        top: calc(50% - 1px);
        left: -950%;
        width: 2000%;
        height: 2px;
        background: linear-gradient(to left, #fff0,#ffffff);
        content: "";
        transform: ${(props)=> props.$direction === "left" ? `rotateZ(-${props.$angle}deg)` : `rotateZ(-${180 - props.$angle}\deg)`} translateX(50%);
    }
    .star:nth-child(2){
        transform: translateX(300px);
        animation-delay: 5.1s;
    }
    .star:nth-child(3) {
        transform: translateX(450px);
        animation-delay: 1s;
    }
`
interface MeteorEffectProps {
    count?: number;
    white?: boolean;
    maxDelay?: number;
    minSpeed?: number;
    maxSpeed?: number;
    angle?: number;
    direction?: "left" | "right";
}
const MAX_STAR_COUNT = 50;

const colors = ["#c77eff", "#f6ff7e", "#ff8d7e", "#ffffff"];
export default function MeteorEffect({ count = 12, white = false, maxDelay = 15, minSpeed = 2 , maxSpeed =  4, angle = 30, direction  = "right" }: MeteorEffectProps) {
    const starCount = count < MAX_STAR_COUNT ? count : MAX_STAR_COUNT;
    const [starInterval, setStarInterval] = useState<number>(0);
    
    

    useEffect(() => {
        const calcStarInterval = () => {
            let innerWidth = window.innerWidth;
            setStarInterval(Math.floor((innerWidth * 1.5) / (count * 5)));
        }
        calcStarInterval();
        window.addEventListener("resize", calcStarInterval);
        return () => {
            window.removeEventListener("resize", calcStarInterval);
        }
    }, [])

    
  return (
      <MeteorEffectLayout $direction={direction} $angle={angle}>
          {(new Array(starCount)).fill(0).map((e, idx) => {
              const left = direction === "left" ? `${Math.random() * count * 5 * starInterval}px` : `${window.innerHeight - Math.random() * count * 5 * starInterval}px`;
              const animationDelay = `${Math.random() * maxDelay}s`;
              const animationDuration = maxSpeed > minSpeed ? `${minSpeed + Math.random() * maxSpeed}s` : `${2 + Math.random() * 4}`; 
              const colorIndex = Math.floor(Math.random() * colors.length - 0.001); // 별 색상
              const size = `${2 + Math.floor(Math.random() * 5)}px`; // 별 크기
              const boxShadow = `0px 0px 10px 3px ${colors[colorIndex]}`; 
              return <div key={idx} style={{left, animationDelay, animationDuration, boxShadow, width : size, height: size }} className='star'></div>
          })}
    </MeteorEffectLayout>
  )
}
profile
웹 개발자

0개의 댓글

관련 채용 정보