[vanila.js] particles landing motion

woolee의 기록보관소·2023년 2월 28일
0

FE 기능구현 연습

목록 보기
32/33

출처 : Vanilla JavaScript Particles Landing Page with Animated Splash Screen

/index.html

<body>

  <div class="splash__screen">
    <div class="left"></div>
    <div class="right">
      <h1 class="percentage">0%</h1>
    </div>
    <div class="progress__bar"></div>
  </div>

  <section class="hero">
    <header>
      <div class="container">
        <h3>Studi-CB</h3>
        <nav>
          <ul>
            <li><a href="">Home</a></li>
            <li><a href="">About</a></li>
          </ul>
        </nav>
      </div>
    </header> 
    <canvas class="hero__canvas"></canvas>
    <div class="main__title">
      <div class="hero__text">
        <h1>Studio-CB</h1>
        <div class="sub__title">
          <h1>Web Development</h1>
          <h1>Explore</h1>
        </div>
        <div class="separator"></div>
        <div class="desc">
          <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis rerum saepe velit aliquid assumenda id voluptatem quis voluptatum nesciunt expedita quod error labore commodi iusto in eius molestiae, itaque odit.</p>
        </div>
      </div>
    </div>
  </section>

  <script type="module" src="/js/app.js"></script>
</body>

/style.css


* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: sans-serif;
  font-weight: 100;
  letter-spacing: -.05em;
  text-decoration: none;
  list-style: none;
  color: white;
}

html, body {
  background-color: #161616;
  width: 100%;
  height: 100%;
}

h1 {
  font-size: 3rem;
}

.hero {
  position: relative;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

canvas {
  position: relative;
  z-index: 0;
}

header {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 50px;
  display: flex;
  justify-content: center;
  align-items: center;
  /* border-bottom: 1px solid red; */
}

header .container {
  display: flex;
  width: 95%;
  justify-content: space-between; 
}

header .container nav ul {
  display: flex;
  width: 100px;
  justify-content: space-between;
}

.main__title {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  padding-top: 30vh;
  display: flex;
  justify-content: center;
}

.hero__text {
  width: 95%;
}

.sub__title {
  display: flex;
  justify-content: space-between;
}

.separator {
  width: 100%;
  height: 1px;
  background-color: #FFFFFF95; // FFFFFF 95% 
  margin-top: 1rem;
}

.desc {
  position: absolute;
  bottom: 1rem;
  display: grid;
  grid-template-columns: repeat(8, 1fr);
}

.desc p {
  grid-column: 1 / span 3;
  font-size: .8rem;
}

.splash__screen {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 10;
}

.splash__screen.complete {
  pointer-events: none;
}

.left {
  position: absolute;
  left: 0;
  top: 0;
  width: 50%;
  height: 100%;
  background-color: #161616;
  transition: transform 1s;
}

.left.active {
  transform: translateX(-100%);
  border-right: 1px solid white;
}

.right {
  position: absolute;
  left: 50%;
  top: 0;
  width: 50%;
  height: 100%;
  background-color: #161616;
  transition: transform 1s;
}

.right.active {
  transform: translateX(100%);
  border-left: 1px solid white;
}

.progress__bar {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  height: 0%;
  width: 1px;
  background-color: white;
  transition: height 1s;
}

.progress__bar.complete {
  opacity: 0;

}

.percentage {
  position: absolute;
  bottom: 0;
  left: 1rem;
}

@media only screen and (max-width: 600px){
  h1 {
    font-size: 2rem;
  }

  .main__title {
    padding-top: 20vh;
  }

  .sub__title {
    height: 100px;
    flex-direction: column;
  }

  .desc p {
    grid-column: 1 / span 6;
  }
}

/js/particles.js

벽을 만났을 때 speed에 -1 값을 곱해줘서 튕겨져 나오는 모션을 그린다.

ctx.strokeStyle = color (외곽선에 색상 넣기)
ctx.fillStyle = color (도형 안에 색상 채워넣기)
ctx.beginPath() : path를 생성한다.
ctx.stroke() : 윤곽선을 이용해서 도형을 그린다.
ctx.moveTo(x, y) : 펜을 (x,y) 좌표로 이동시킨다.
ctx.lineTo(x, y) : 현재 드로잉 위치에서 (x,y) 좌표까지 선을 그린다.

window.innerWidth : 픽셀로 창 내부의 너비를 반환 (더 정확히는, layout viewport의 너비 반환)
Window.devicePixelRatio : 현재 표시 장치의 물리적 픽셀과 css 픽셀 비율을 반환

ctx.clearRect(x, y, width, height) : 특정 부분을 지우는 직사각형, 이 지워진 부분은 완전히 투명해진다.

호나 원을 그리기 위해서는 arc() 혹은 arcTo() 메서드를 사용한다.
arc(x, y, radius, startAngle, endAngle, anticlockwise)

  • (x,y) 위치에 원점을 두고, 반지름 r을 가지고 startAngle에서 시작해서 endAngle에서 끝나며, 주어진 anticlockwise 방향으로 (기본값은 시계 방향)

Math.PI 는 원의 둘레와 지름의 비율인 약 3.14159 의 값을 가진다.

  • Math.PI / 180 이 1도, Math.PI는 180도. 여기에 2를 곱하니까 360도

window.requestAnimationFrame()

  • 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트가 진행되기 전에 해당 애니메이션을 업데이트하는 함수를 호출

Applying styles and colors
Drawing paths
Moving the pen
Lines

Window.innerWidth

Drawing shapes with canvas
Arcs
window.requestAnimationFrame()

// particles.js 

const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')

let particles = []
let randomMaxSpeed = 5

class Particle {
  constructor(){
    this.reset() 
    this.speedY = Math.random() > .5 ? (Math.random() * randomMaxSpeed) * -1 : (Math.random() * randomMaxSpeed)
    this.speedX = Math.random() > .5 ? (Math.random() * randomMaxSpeed) * -1 : (Math.random() * randomMaxSpeed)
  }

  reset(){
    this.coordinates = {
      x: Math.random() * canvas.width, 
      y: Math.random() * canvas.height,
    }
  }

  move(){
    if(this.coordinates.x >= canvas.width || this.coordinates.x <= 0){
      this.speedX = this.speedX * -1 
    }
    if(this.coordinates.y >= canvas.height || this.coordinates.y <= 0){
      this.speedY = this.speedY * -1 
    }
    // if(this.coordinates.x <= 0){
    //   this.speedX = this.speedX * -1 
    // }
    // if(this.coordinates.y <= 0){
    //   this.speedY = this.speedY * -1 
    // }

    for(let i=0; i<particles.length; i++){
      let { x, y } = this.coordinates

      if(Math.abs(x - particles[i].coordinates.x) <= 200 && Math.abs(y - particles[i].coordinates.y) <= 200){
        
        ctx.strokeStyle = `#03c0ff25`
        ctx.beginPath()
        ctx.moveTo(x, y)
        ctx.lineTo(particles[i].coordinates.x, particles[i].coordinates.y)
        ctx.stroke()
      }
    }

    this.coordinates.x += this.speedX
    this.coordinates.y += this.speedY
  }
}

function setDimensions(){
  particles = [] 

  canvas.width = window.innerWidth * window.devicePixelRatio
  canvas.height = window.innerHeight * window.devicePixelRatio
  canvas.style.width = `100%`
  canvas.style.height = `100%`

  let w = window.innerWidth
  let particleTotal = w > 1000 ? 300 : 150 

  for(let i=0; i<particleTotal; i++){
    let particle = new Particle() 
    particles.push(particle)
  }
}

function animate(){
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  for(let i=0; i<particles.length; i++){

    let { x, y } = particles[i].coordinates

    particles[i].move()
    ctx.beginPath()
    ctx.arc(x, y, 3, 0, 2 * Math.PI)
    ctx.stroke()
  }

  requestAnimationFrame(animate)
}

export {
  setDimensions,
  animate,
  particles
}

/js/app.js

clearTimeout을 사용하기 위해
setTimeout을 사용한 부분은 Promise로 묶은 뒤,
async await을 사용해서 clearTimeout을 이 내부에 사용했다.

// app.js

import { setDimensions, animate } from "./particles.js"

const splashScreen = document.querySelector('.splash__screen')
const splashLeft = document.querySelector('.left')
const splashRight = document.querySelector('.right')
const progressBar = document.querySelector('.progress__bar')
const percentage = document.querySelector('.percentage')

let loading = true 

/**
 * https://inpa.tistory.com/entry/jQuery-📚-브라우저-resize-이벤트-사용법-최적화
 */

window.addEventListener('resize', setDimensions)

setDimensions()
animate()

let timeoutList = []

// function setup(){
//   timeoutList[0] = setTimeout(() => {
//     progressBar.style.height = '40%'
//   }, 2000)
//   timeoutList[1] = setTimeout(() => {
//     progressBar.style.height = '80%'
//   }, 4000)
//   timeoutList[2] = setTimeout(() => {
//     progressBar.style.height = '100%'
//   }, 5000)
//   timeoutList[3] = setTimeout(() => {
//     splashLeft.classList.add('active')
//     splashRight.classList.add('active')
//     progressBar.classList.add('complete')
//     splashScreen.classList.add('complete')
//     loading = false 
//     clearTimeoutList()
//   }, 6000)
// }

let setup = new Promise((resolve, reject) => {
  timeoutList[0] = setTimeout(() => {
    progressBar.style.height = '40%'
  }, 2000)
  timeoutList[1] = setTimeout(() => {
    progressBar.style.height = '80%'
  }, 4000)
  timeoutList[2] = setTimeout(() => {
    progressBar.style.height = '100%'
  }, 5000)
  timeoutList[3] = setTimeout(() => {
    splashLeft.classList.add('active')
    splashRight.classList.add('active')
    progressBar.classList.add('complete')
    splashScreen.classList.add('complete')
    loading = false 
    resolve('complete randing') // 내가 원하는 시점은 여기이므로
  }, 6000)
})

async function clearTimeoutList(){
  try {
    let result = await setup
    // console.log(result)
    // console.log(timeoutList)
    for(let i=0; i<timeoutList.length; i++){
      window.clearTimeout(timeoutList[i])
    }
    timeoutList = []
    // console.log(timeoutList)
  } catch {
    console.log('setTimeout 함수를 삭제하지 못했습니다.')
  }
}

function percentageTracker(){
  // console.log(loading) 100번 실행됨 
  if(loading){
    let { height, top } = progressBar.getBoundingClientRect()
    let p = Math.ceil((height / window.innerHeight) * 100)
    percentage.textContent = `${p}%`
    percentage.style.transform = `translateY(calc(${top - window.innerHeight}px))`
    requestAnimationFrame(percentageTracker)
  }
}

// setup()
clearTimeoutList()
percentageTracker()
profile
https://medium.com/@wooleejaan

0개의 댓글