이제 CSS/JS 복습의 마무리에 다다랐다. 최종장인 3D 스크롤을 구현하는것을 연습하면서 그동안 진행했던 CSS/JS 학습을 총정리하면서 마무리를 하려고 한다.
화면의 3D 시점은 perspective
라는 속성으로 인하여 결정된다.
먼저 스크롤을 통하여 캐릭터가 전진하는 것을 구현하고 싶은 것이므로, 전체 body
의 높이를 500vw
로 결정하여 스크롤을 충분히 할 수 있는 공간을 만들어 둔다.
body{
height:500vw;
font-family: Arial, Helvetica, sans-serif;
color:#555;
background: #fff000;
-webkit-overflow-scrolling: touch;
transition: 0.5s;
}
해당 공간에 3D 공간을 만들어야 하므로, stage
컨테이너를 만들어 3D 공간으로 만들어주고, 스크롤에 따라서 위치가 변하면 안되므로 position
은 fixed
로 고정한다. 그리고 가로와 세로를 각각 100vw
와 100vh
로 설정하여 우리 눈에 화면이 꽉 차서 보일 수 있도록 한다.
다음으로는 3D 모형으로 보이는 방 안을 구성할 차례인데,
아래 그림과 같이 가장 앞의 벽은 Z-index
를 두어 보이는 순서 차이가 존재하도록 만들고, 위 아래 벽을 이용하여 3D 입체 공간을 더욱 현실적으로 표현한다.
왼쪽 벽과 오른쪽 벽을 구현하는 것이 조금 어렵게 느껴질 수 있는데, Y축을 기준으로 90도 회전시켜 둔 다음 Z축으로 가로 벽의 길이만큼 이동 시키면 위와 같이 3D 모형으로 구현할 수 있다.이 때, Y축을 기준으로 회전시키면 축이 변화하기 때문에 Z축으로 이동하는 방향을 신경써야 한다는 것을 유의해야 한다. (왜 안돼?!!?) 하지 말기!
다음으로는 위와 같이 마우스를 이동하는 곳에 따라서 시점이 이동하게끔 하는 것을 구현한다.
이 기능은 굳이 구현하지 않아도 되나, 이런 효과가 있으면 3D 공간에 와 있는 듯한 느낌을 극대화시킬 수 있으므로 구현하는 것이 좋다! 내가 구현하고자 하는 웹페이지가 조금 더 디테일하게 표현되는 효과를 준다.
해당 효과는 자바스크립트를 이용하여 줄 수 있는데,
window.addEventListener('mousemove', function(e){
mousePos.x = -1 + (e.clientX / window.innerWidth) * 2;
mousePos.y = 1 - (e.clientY / window.innerHeight) * 2;
stageElem.style.transform = 'rotateX(' + mousePos.y * 5 + 'deg) rotateY(' + mousePos.x * 5 +'deg)'
})
mousemove
이벤트 핸들러 함수를 작성하여 효과를 줄 수 있다.
두번째, 세번째 줄과 같은 형식을 앞으로 많이 이용하게 될 수도 있는데, 정 가운데를 0,0으로 두고, 위 - 아래를 1 ~ -1 범위로 설정, 왼쪽 - 오른쪽을 -1 ~ +1 범위로 설정하여 stage 화면 전체를 회전시키도록 한 것이다.
지금은 각도를 적정한 각도로 조정한다고 2를 곱해놓은 상태인데, 더욱 크거나 작게 만들고 싶다면 배수를 조절할 수 있다.
window.innerWidth
는 현재 브라우저의 가로 길이를 나타내는 것으로, body의 픽셀을 세는 것보다 변화하는 브라우저 창에 대하여 조금 더 자유롭게 대응할 수 있다.
stageElem.addEventListener('click', function(e){
new Character({
xPos : e.clientX / window.innerWidth * 100,
speed: Math.random() * 0.5 + 0.2
});
});
stage DOM 요소를 클릭할 때마다 캐릭터가 생성되도록 만드는 이벤트 핸들러이다. 캐릭터 객체를 만들어서 위치와 그의 스피드를 결정해 주는 것으로, 캐릭터가 생성되기 위해서 Character
객체 생성자를 보아야 한다.
function Character(info) {
this.mainElem = document.createElement('div');
this.mainElem.classList.add('character');
this.mainElem.innerHTML = ''
+ '<div class="character-face-con character-head">'
+ '<div class="character-face character-head-face face-front"></div>'
+ '<div class="character-face character-head-face face-back"></div>'
+ ...(이하생략);
document.querySelector('.stage').appendChild(this.mainElem);
//마우스 클릭하는 곳으로 위치 설정한다.
this.mainElem.style.left = info.xPos + '%';
// 스크롤 중인지 아닌지 체크 가능!
this.scrollState = false;
// 바로 이전 스크롤 위치
this.lastScrollTop = 0;
this.xPos = info.xPos;
//객체 속성으로 위치 지정.
this.speed = info.speed;
this.direction;
// 좌우 이동 중인지 아닌지
this.runningState = false;
this.rafId;
this.init();
}
캐릭터 생성 함수에는 정말 많은 내용이 들어 있다.
먼저 div
태그에 해당하는 캐릭터 div 요소들을 추가해 주어야 하므로, classList.add('character')
를 통하여 캐릭터라는 클래스를 가진 div
태그를 추가해준다. 결국 stage에 추가해주어야 하므로 appendChild
메소드를 이용하여 mainElem
을 추가해준다. 이 때, 왼쪽으로부터 얼마나 떨어져 있을지를 결정해주는 info.xPos
를 결정해준다.
여기서 왜 자꾸 this라는 것을 붙일까? 라는 궁금증이 생길 수 있다.
this가 의미하는 것은 해당 객체에 속하는 속성임을 나타내기 위해서 계속하여 작성하는 것으로, this를 콘솔에 찍으면
Character
이 나온다. 캐릭터에 대한 속성으로 받아들이면 된다.
그리고 이후에는 마우스가 스크롤 중인지 아닌지, 이전 스크롤 위치를 저장하는 것, 객체의 스피드를 지정하는 것, 방향 결정, 뛰고 있는 상태인지 아닌지를 결정하는 프로퍼티들이 존재하며 마지막의 init()
을 통해 메소드를 프로토타입으로 지정해둔다.
메소드를 프로토타입으로 지정한 내용은 아래와 같다.
Character.prototype = {
constructor: Character,
init: function () {
const self = this;
//안에서 this쓰면 window 전역객체 가리키기 때문에 캐릭터 객체를 self에 넣어서 사용하는 것이다.
}
};
이 프로토타입 함수에서 이후 진행할 스크롤 상태와 캐릭터가 달리는 형태를 결정하는 작업을 진행할 것이다.
이 때, 미리 self
라는 변수를 만들고 this 객체를 넣어 두었는데,
프로토타입 메소드 안에서 this
라는 것을 사용할 때 window 전역객체를 가리켜 undefined가 뜨는 현상을 방지하기 위해서 캐릭터 객체인 this
를 self
에 미리 넣어둔다.
캡쳐한 화면에서 스크롤바를 담은 컨테이너가 얇은 편이라 잘 보이지는 않지만 스크롤을 할 때 파란색 게이지가 채워지고, 스크롤을 다시 위로 올리면 파란색 게이지바가 줄어드는 것을 볼 수 있다.
이것을 구현하기 위해서 가장 먼저 해두어야 하는 작업이 있다.
브라우저 크기가 변할 때마다 그에 대한 스크롤 가능 범위가 변한다
이 사실을 알고 있으면 브라우저 크기 변화에 대응하는 스크롤 변화를 컨트롤 할 수 있다.
그렇기 때문에 브라우저 resize
이벤트가 발생하게 되면 스크롤 범위를 다시 세팅할 수 있도록 함수를 작성해주어야 한다.
function resizeHandler(){
maxScrollValue = document.body.offsetHeight -
window.innerHeight
}
document.body.offsetHeight
는 body 요소의 전체 사이즈를 의미한다. 여기서 window.innerHeight
브라우저의 현재 높이를 빼게 되면 스크롤을 할 수 있는 범위가 도출된다.
이와 같은 계산 방법은 스크롤 이벤트를 이용한 다양한 곳에서 활용될 수 있으므로 기억하고 있으면 매우 좋다.
window.addEventListener('resize', resizeHandler);
resizeHandler();
이렇게 정리하면 보고 있는 브라우저에서 resize
이벤트가 발생하였을 때 resizeHandler
이벤트가 발생하여서 스크롤 가능 범위를 다시 정해준다.
이제 스크롤 범위를 잘 정해 주었으니 그에 따라서 색이 변화하는 컨테이너를 만들기만 하면 된다.
window.addEventListener('scroll', function(){
const scrollPer = pageYOffset / maxScrollValue
const zMove = scrollPer * 980 - 490;
//0부터 1000까지 비율로 설정!
houseElem.style.transform = 'translateZ(' + zMove + 'vw)';
//progressbar 처리
barElem.style.width = scrollPer * 100 + '%';
})
pageYOffset/maxScrollValue
를 통하여 0부터 1까지의 비율로 설정할 수 있다. 왜냐하면 pageYOffset
은 현재 스크롤한 범위를 나타내기 때문이다. 그 값에 100을 곱하면 퍼센트로 결정되므로, barElem.style.width
를 이용하여 색을 칠할 수 있다.여기서 houseElem.style.transform
은 스크롤을 하면서 Z방향으로 축을 이동하면서 방을 이동하는 효과를 의미한다.
처음에 Z인덱스를 -490만큼 이동시켜 놓았으니까 비율에서 490을 제거하는 것이다.
Character.prototype = {
constructor: Character,
init: function () {
const self = this;
window.addEventListener('scroll', function () {
clearTimeout(self.scrollState);
if (!self.scrollState) {
self.mainElem.classList.add('running');
}
self.scrollState = setTimeout(function () {
self.scrollState = false;
self.mainElem.classList.remove('running');
}, 500);
});
}
스크롤 이벤트가 발생할 때, running
상태의 클래스를 덧붙여주는 것으로 달려가는 모습을 구현할 수 있다.
이 때 시간과 관련하여 setTime
함수를 사용한 것을 볼수 있는데 이것은 왜 사용한 것일까?
스크롤 이벤트가 발생할 때마다 running 클래스를 add하게 되면 효율적인 코드가 되지 않는다.
물론 setTimeout
함수를 사용하지 않아도 된다. 그러나 스크롤을 할 때마다 정말 많이 running
클래스를 실행한다면 실행 시간이 번거로운 코드가 될 것이다. 이를 방지하기 위해서, scrollState
라는 변수를 설정해두고 디폴트값으로 false
라고 둔다. 이 때 setTimeout
함수에서 반환하는 ID를 가지게 되어서 scrollState
함수에 양수의 수가 들어가게 된다. scrollState
는 True 상태가 된다.
만약 다시 스크롤을 진행할 경우, clearTimeout
함수가 다시 스크롤 시 수행되어서 0이 리턴, scrollState
함수는 다시 false 상태가 된다. 만약 스크롤을 하고, 0.5s동안 아무 일도 일어나지 않으면 콜백함수를 수행하는데, 다시 스크롤 상태를 false
로 만들어 주고, running
클래스를 지워주는 것이다. 이런 방식으로 running
클래스가 무분별하게 추가되고 사라지는 것을 방지할 수 있다.
그럼 running
클래스에는 무엇이 있을까?
@keyframes ani-running-leg {
from {
transform: rotateX(-30deg);
}
to {
transform: rotateX(30deg);
}
}
@keyframes ani-running-arm {
from {
transform: rotateY(30deg);
}
to {
transform: rotateY(-30deg);
}
}
.character.running .character-leg-right { animation: ani-running-leg 0.2s alternate infinite linear; }
.character.running .character-leg-left { animation: ani-running-leg 0.2s alternate-reverse infinite linear; }
.character.running .character-arm { animation: ani-running-arm 0.2s alternate infinite linear; }
running 클래스가 붙으면 다리와 팔이 움직이는 애니메이션을 등록해놓았다.
이제 스크롤을 아래로 하면, 캐릭터가 뒷모습을 보이면서 걸어가고, 스크롤을 위로 하면 캐릭터가 앞모습을 보이면서 걸어가는 것으로 구현해야 한다.
스크롤을 위로 올리는지 아래롤 올리는지 알기 위해서는 스크롤 변화값을 계산하면 된다.
if (self.lastScrollTop > pageYOffset) {
self.mainElem.setAttribute('data-direction', 'backward');
} else {
self.mainElem.setAttribute('data-direction', 'forward');
}
self.lastScrollTop = pageYOffset;
스크롤 위치를 lastScrollTop
이라는 변수에 저장을 해 둔다. 이전 스크롤 위치가 더욱 크다면, 스크롤을 올리는 상태이므로 뒷모습을 보이도록 하고, 현재 스크롤 위치가 더욱 크다면 스크롤을 내리는 상태이므로 앞모습을 보이게 한다.
좌우 방향키를 누르면 움직이고, 그 자리에 캐릭터가 잘 존재할 수 있도록 코드를 만들어보자.
window.addEventListener('keydown', function (e) {
if (self.runningState) return;
if (e.keyCode == 37) {
self.direction = 'left';
self.mainElem.setAttribute('data-direction', 'left');
self.mainElem.classList.add('running');
self.run(self);
self.runningState = true;
} else if (e.keyCode == 39) {
self.direction = 'right';
self.mainElem.setAttribute('data-direction', 'right');
self.mainElem.classList.add('running');
self.run(self);
self.runningState = true;
}
});
키코드 37은 왼쪽, 키코드 39는 오른쪽을 가리킨다.
키보드를 눌렀을 때 run 함수를 실행시켜주는데, run 메소드가 어떤 일을 하는지 보자.
run: function (self) {
if (self.direction == 'left') {
self.xPos -= self.speed;
} else if (self.direction == 'right') {
self.xPos += self.speed;
}
if (self.xPos < 2) {
self.xPos = 2;
}
if (self.xPos > 88) {
self.xPos = 88;
}
self.mainElem.style.left = self.xPos + '%';
self.rafId = requestAnimationFrame(function () {
self.run(self);
});
}
xPos를 speed만큼 빼거나 더해주는 방식으로 위치를 구현한다. 단, 일정 범위 이상을 나가면 안되므로 범위 제한을 걸어둔다. 걸어다니는 애니메이션은 keydown
이 아무리 빨리 되어도 부드럽게 프레임 넘기듯이 수행되지 않기 때문에 requestAnimationFrame
을 사용하여 부드럽게 프레임 넘기든 애니메이션이 실행되도록 한다.
이제 키를 떼면 캐릭터가 멈추어야 한다.
window.addEventListener('keyup', function (e) {
self.mainElem.classList.remove('running');
cancelAnimationFrame(self.rafId);
self.runningState = false;
});
키 눌렀던 것을 떼었을 때, running 클래스를 지우고, 애니메이션 프레임 진행되던 것을 취소하기 위해서 cancleAnimationFrame
메소드를 실행한다.
이렇게 CSS와 JS만으로 풍부하고 재미있는 3D 효과를 구현해낼 수 있다. 기본적인 키보드 누르기, 클릭 및 스크롤 이벤트를 배웠으니 여러가지 방면에서 활용해볼 수 있을 것으로 기대된다.