Intro
어느 날 포트폴리오 웹페이지를 만들다 문득 별이 빛나는 듯한 이펙트를 추가하고 싶다는 생각이 들었습니다.
그래서 인터넷을 찾아보다 좋은 튜토리얼을 발견하여 조금 수정해서 적용해봤습니다.
적용 후 결과가 상당히 마음에 들어서 이번 시간엔 밤하늘의 별 이펙트 를 추가하는 방법에 대해 포스팅하도록 하겠습니다.
코드는 아래의 링크를 참고하여 작성했습니다.
http://timothypoon.com/blog/2011/01/19/html5-canvas-particle-animation/
HTML5 Canvas Particle Animation
Learn how to make a beautiful HTML5 canvas particle animation system of glowing stars and a parallax background.
timothypoon.com
변수
제가 원하던 이펙트와 가장 가깝지만 필요하지 않은 기능과 몇 가지의 버그를 고쳐봤습니다.
우선 코드를 분석해가며 제가 수정한 부분들과 함께 정리해 보겠습니다.
먼저 변수 초기화 코드입니다.
var WIDTH = window.innerWidth;
var HEIGHT = window.innerHeight;
var MAX_PARTICLES = (WIDTH * HEIGHT) / 20000;
var DRAW_INTERVAL = 60;
var canvas = document.querySelector('.background');
var context = canvas.getContext('2d');
var gradient = null;
var pixies = new Array();
코드 해석
- WIDTH: 창의 가로 길이를 저장
- HEIGHT: 창의 세로 길이를 저장
- MAX_PARTICLES: 화면에 표시할 별의 개수
- DRAW_INTERVAL: 프레임 출력 후 다음 프레임 출력까지의 대기시간
- canvas: 별이 그려지게 될 canvas 태그 영역
- gradient: 별의 빛나는 효과 저장 (프레임마다 별의 수만큼 변수를 생성하지 않기 위해 선언한 것 같습니다.)
- pixies: 별 하나하나를 저장하는 배열
수정 사항
- MAX_PARTICLES 초기값 수정: 초기값을 상수에서 화면 크기에 비례한 값으로 조정하였습니다.
- container 변수 삭제: 부모 컨테이너의 크기는 따로 조정하지 않아도 되도록 설계하여 삭제했습니다.
- canvas querySelector 수정: 제가 작성한 페이지의 클래스에 맞도록 쿼리를 수정하였습니다.
다음은 화면 크기 조정과 관련된 코드입니다.
function setDimensions(e) {
WIDTH = window.innerWidth;
HEIGHT = window.innerHeight;
MAX_PARTICLES = (WIDTH * HEIGHT) / 20000;
canvas.width = WIDTH;
canvas.height = HEIGHT;
console.log("Resize to " + WIDTH + "x" + HEIGHT);
}
setDimensions();
window.addEventListener('resize', setDimensions);
코드 해석
- function setDimensions: 화면의 너비, 높이를 읽어 변수 값을 수정, 캔버스의 크기 조정
- setDimensions(); : 페이지가 처음 로드되었을 때 1회에 한해 갱신
- window.addEventListener('resize', setDimensions): 창 크기 변경 이벤트 발생 시 위의 함수를 호출시키도록 등록
수정 사항
- container 크기 조정 삭제: container 변수를 삭제함에 따라 크기를 조정하는 코드도 삭제하였습니다.
- MAX_PARTICLES 변경값 수정: 위의 변수 초기값 변경의 이유와 같습니다.
Circle 함수
다음은 Circle 함수입니다.
사실 키워드는 function인데 들어있는 내용을 보면 함수보단 객체에 더 가까운 것 같습니다.
우선 settings와 reset 코드입니다.
this.settings = {ttl:8000, xmax:5, ymax:2, rmin:8, rmax:15, drt:1};
this.reset = function() {
this.x = WIDTH*Math.random(); //X 위치 랜덤 (0 ~ WIDTH)
this.y = HEIGHT*Math.random(); //Y 위치 랜덤 (0 ~ HEIGHT)
this.r = ((this.settings.rmax-1)*Math.random()) + 1; //반지름 크기 랜덤 (1 ~ rmax)
this.dx = (Math.random()*this.settings.xmax) * (Math.random() < .5 ? -1 : 1); //X 이동거리 랜덤 (-xmax ~ xmax)
this.dy = (Math.random()*this.settings.ymax) * (Math.random() < .5 ? -1 : 1); //Y 이동거리 랜덤 (-ymax ~ ymax)
this.hl = (this.settings.ttl/DRAW_INTERVAL)*(this.r/this.settings.rmax); //총 생존 시간 (반지름 크기에 비례)
this.rt = 0; //현재 생존 시간 (0 -> hl -> 0)
this.settings.rt = Math.random()+1; //노화 속도 (1 ~ 2)
this.stop = Math.random()*.2+.4; //음영 범위 (0.4 ~ 0.6)
}
this.fade = function() {
this.rt += this.settings.rt; //노화 진행
if(this.rt >= this.hl) {
this.rt = this.hl;
this.settings.rt = this.settings.rt*-1;
} else if(this.rt < 0) {
this.reset(); //수명이 다하면 새로운 위치에 생성
}
}
코드 해석
- ttl: 별이 살아있는 시간
- xmax: 별의 최대 x 이동거리
- ymax: 별의 최대 y 이동거리
- rmin: 최소 반지름 크기
- rmax: 최대 반지름 크기
- drt: 별의 노화 속도
- x: x 좌표
- y: y 좌표
- r: 반지름
- dx: x 이동거리
- dy: y 이동거리
- hl: 밝기가 최대가 되는 프레임
- rt: 현재 밝기 (0 -> hl -> 0)
- stop: 밝기가 변화하는 반지름 지점
수정 사항
- 필요 없는 변수 제거: 사용하지 않는 xdef, ydef, xdrift, ydrift, random, blink 변수를 제거했습니다.
- rmin 변수 추가: 최소 반지름인 rmin 변수를 추가했습니다.
- x, y 초기값 조건문 제거: random 변수 제거에 따라 조건문을 제거했습니다.
- rt 초기값 조건문 제거: 처음 밝기를 랜덤이 아닌 항상 0이 되도록 수정했습니다.
다음은 그리기 관련 함수들입니다.
this.fade = function() {
this.rt += this.settings.drt; //노화 진행
if(this.rt >= this.hl) {
this.rt = this.hl;
this.settings.drt = this.settings.drt*-1;
} else if(this.rt < 0) {
this.reset(); //수명이 다하면 새로운 위치에 생성
}
}
this.draw = function() {
var newo = (this.rt/this.hl); //밝기 (0 ~ 1)
context.beginPath();
context.arc(this.x, this.y, this.r, 0, Math.PI * 2, true); //(x, y) 좌표에 반지름 r 크기의 원 그림
context.closePath();
var cr = this.r*newo; //밝기에 따른 반지름
gradient = context.createRadialGradient(this.x, this.y, 0, this.x, this.y, (cr < this.settings.rmin) ? this.settings.rmin : cr);
gradient.addColorStop(0.0, 'rgba(255,255,255,'+newo+')');
gradient.addColorStop(this.stop, 'rgba(77,101,181,'+(newo*.6)+')');
gradient.addColorStop(1.0, 'rgba(77,101,181,0)');
context.fillStyle = gradient;
context.fill();
}
this.move = function() {
this.x += (1 - this.rt/this.hl)*this.dx;
this.y += (1 - this.rt/this.hl)*this.dy;
if(this.x > WIDTH || this.x < 0) this.dx *= -1;
if(this.y > HEIGHT || this.y < 0) this.dy *= -1;
}
코드 해석
- this.fade: 밝기를 0 -> Max -> 0 으로 조정시켜줍니다. 밝기가 0에서 더 어두워지면 새로운 위치에 다시 별을 출력하도록 reset 함수를 호출해줍니다.
- this.draw: 설정된 반지름에 해당하는 원에 gradient를 통한 명암을 주고 화면에 출력합니다.
- this.move: 현재 x, y 좌표에 이동 거리에 비례한 값을 추가합니다.
수정 사항
- reset 함수를 호출하지 않는 버그 수정
기존엔 한번 별이 만들어지면 밝기의 변화량과 이동거리만 갱신하는 방식이었습니다.
코드엔 reset을 호출하는 조건문이 있었지만, 다른 조건문에 먼저 걸려 절대 호출되지 않는 버그가 있었습니다.
따라서 해상도가 변경되어도, 변경된 해상도에 맞춰 별들이 퍼지려면 별들이 알아서 이동하는것 밖에 없었습니다.
그래서 별의 밝기가 0이 되면 reset 함수를 호출하여 넓어진 해상도에도 별들이 퍼지도록 코드를 수정했습니다. - 밝기 변화 로직 변경: draw 함수에 있던 밝기 변화량 지정 및 reset 코드를 fade 함수로 이동시켰습니다.
- 최소 반지름 추가: 반지름이 너무 작아 잘 보이지 않는 별들이 없도록 추가했습니다.
별 생성 및 출력 코드
마지막으로 별 생성 및 출력 코드입니다.
function draw() {
context.clearRect(0, 0, WIDTH, HEIGHT);
for(var i=pixies.length; i<MAX_PARTICLES; i++) {
pixies.push(new Circle());
pixies[i].reset();
}
for(var i = 0; i < MAX_PARTICLES; i++) {
pixies[i].fade();
pixies[i].move();
pixies[i].draw();
}
}
setInterval(draw, DRAW_INTERVAL);
코드 해석
- function draw: 화면을 초기화 하고 부족한 별을 생성하며 별들을 출력합니다.
- setInterval: draw 함수를 DRAW_INTERVAL msec 마다 호출합니다.
수정 사항
- 화면의 출력되는 별의 수 동적 변경
기존엔 MAX_PARTICLES 변수에 상수가 들어있어 화면의 크기가 크든 작든 지정된 수의 별이 출력되었습니다.
때문에 별이 너무 퍼져있거나 밀집되어있는 현상이 생겼고, 이를 해결하기 위해 기존의 정해진 수만큼의 별을 생성하는 코드를 화면 크기에 비례하여 생성하도록 변경했습니다.
최종 코드 및 결과
수정이 모두 끝났습니다.
최종 코드는 다음과 같습니다.
var WIDTH = window.innerWidth;
var HEIGHT = window.innerHeight;
var MAX_PARTICLES = (WIDTH * HEIGHT) / 20000;
var DRAW_INTERVAL = 60;
var canvas = document.querySelector('.background');
var context = canvas.getContext('2d');
var gradient = null;
var pixies = new Array();
function setDimensions(e) {
WIDTH = window.innerWidth;
HEIGHT = window.innerHeight;
MAX_PARTICLES = (WIDTH * HEIGHT) / 20000;
canvas.width = WIDTH;
canvas.height = HEIGHT;
console.log("Resize to " + WIDTH + "x" + HEIGHT);
}
setDimensions();
window.addEventListener('resize', setDimensions);
function Circle() {
this.settings = {ttl:8000, xmax:5, ymax:2, rmin:8, rmax:15, drt:1};
this.reset = function() {
this.x = WIDTH*Math.random(); //X 위치 랜덤 (0 ~ WIDTH)
this.y = HEIGHT*Math.random(); //Y 위치 랜덤 (0 ~ HEIGHT)
this.r = ((this.settings.rmax-1)*Math.random()) + 1; //반지름 크기 랜덤 (1 ~ rmax)
this.dx = (Math.random()*this.settings.xmax) * (Math.random() < .5 ? -1 : 1); //X 이동거리 랜덤 (-xmax ~ xmax)
this.dy = (Math.random()*this.settings.ymax) * (Math.random() < .5 ? -1 : 1); //Y 이동거리 랜덤 (-ymax ~ ymax)
this.hl = (this.settings.ttl/DRAW_INTERVAL)*(this.r/this.settings.rmax); //총 생존 시간 (반지름 크기에 비례)
this.rt = 0; //현재 생존 시간 (0 -> hl -> 0)
this.settings.drt = Math.random()+1; //노화 속도 (1 ~ 2)
this.stop = Math.random()*.2+.4; //음영 범위 (0.4 ~ 0.6)
}
this.fade = function() {
this.rt += this.settings.drt; //노화 진행
if(this.rt >= this.hl) {
this.rt = this.hl;
this.settings.drt = this.settings.drt*-1;
} else if(this.rt < 0) {
this.reset(); //수명이 다하면 새로운 위치에 생성
}
}
this.draw = function() {
var newo = (this.rt/this.hl); //밝기 (0 ~ 1)
context.beginPath();
context.arc(this.x, this.y, this.r, 0, Math.PI * 2, true); //(x, y) 좌표에 반지름 r 크기의 원 그림
context.closePath();
var cr = this.r*newo; //밝기에 따른 반지름
gradient = context.createRadialGradient(this.x, this.y, 0, this.x, this.y, (cr < this.settings.rmin) ? this.settings.rmin : cr);
gradient.addColorStop(0.0, 'rgba(255,255,255,'+newo+')');
gradient.addColorStop(this.stop, 'rgba(77,101,181,'+(newo*.6)+')');
gradient.addColorStop(1.0, 'rgba(77,101,181,0)');
context.fillStyle = gradient;
context.fill();
}
this.move = function() {
this.x += (1 - this.rt/this.hl)*this.dx;
this.y += (1 - this.rt/this.hl)*this.dy;
if(this.x > WIDTH || this.x < 0) this.dx *= -1;
if(this.y > HEIGHT || this.y < 0) this.dy *= -1;
}
}
function draw() {
context.clearRect(0, 0, WIDTH, HEIGHT);
for(var i=pixies.length; i<MAX_PARTICLES; i++) {
pixies.push(new Circle());
pixies[i].reset();
}
for(var i = 0; i < MAX_PARTICLES; i++) {
pixies[i].fade();
pixies[i].move();
pixies[i].draw();
}
}
setInterval(draw, DRAW_INTERVAL);
결과도 아주 만족스럽게 나왔습니다.
녹화 상으론 프레임이 끊기는 것처럼 보이지만 실제로는 부드럽게 잘 움직입니다.

질문이나 잘못된 부분은 언제든지 댓글로 남겨주세요.
이상으로 포스팅을 마치겠습니다.
'Tips > Front end' 카테고리의 다른 글
[NPM] npm build 이후 serve 시 PSSecurityException 뜰 때 (0) | 2022.12.02 |
---|---|
[ESLint] Delete`CR` 에러 폭탄 해결하기 (End of Line 일괄 변경, 기본값 설정하기) (0) | 2022.10.30 |
[Front-End] Prettier 단축키 (0) | 2022.01.27 |
[CSS] 애니메이션이 떨릴 때 (will-changed) (0) | 2022.01.12 |
[HTML] Lottie Web Player를 사용하여 움직이는 이미지 출력 (0) | 2021.06.02 |