출처 : Vanilla JavaScript Particles Landing Page with Animated Splash Screen
<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>
* {
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;
}
}
벽을 만났을 때 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)
Math.PI 는 원의 둘레와 지름의 비율인 약 3.14159 의 값을 가진다.
window.requestAnimationFrame()
Applying styles and colors
Drawing paths
Moving the pen
Lines
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
}
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()