[CSS 3D] 인터랙티브 웹 효과 구현하기 (1)
+ 이 글의 내용은 인프런의 [인터랙티브 웹 개발 제대로 시작하기] 강좌의 '종합예제' 부분 내용을 담고 있습니다.
1. 3D 공간 작성하기
가장 먼저 3D 공간을 작성합니다. 양옆에는 벽이 있고, 정면에 벽이 세 개 있는 구조입니다.
마크업은 world > stage > house 구조로 이루어져있습니다.
<div class="world">
<div class="stage">
<div class="house">
<div class="house__wall house__wall--left"></div>
<div class="house__wall house__wall--right"></div>
<div class="house__wall house__wall--front house__wall--front-a">
<div class="house__contents">
<h2 class="house__contents-title">
Hello!
</h2>
</div>
</div>
<div class="house__wall house__wall--front house__wall--front-b">
<div class="house__contents">
<h2 class="house__contents-title">
Bonjour!
</h2>
</div>
</div>
<div class="house__wall house__wall--front house__wall--front-c">
<div class="house__contents">
<h2 class="house__contents-title">
Namaste!
</h2>
</div>
</div>
<div class="house__wall house__wall--front house__wall--front-d">
<div class="house__contents">
<h2 class="house__contents-title">
안녕하세요!
</h2>
</div>
</div>
</div>
</div>
</div>
다음은 CSS 차례입니다.
일단 윈도 화면을 다 덮을 거니까 html에게 height:100%
를 줍니다.
html {
height: 100%;
}
body {
margin: 0;
padding: 0;
min-height: 100%;
background: #49b293;
-webkit-overflow-scrolling: touch;
}
그리고 3D를 구현할 수 있도록, .world에다 perspective: 1000px
속성을 줍니다.
이때, position:fixed; width: 100vw; height:100vh;
로 항상 꽉찬 상태로 보이게끔 합니다.
.world {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
perspective: 1000px;
}
그 다음은 .stage 차례입니다.
.world가 3D 공간 전체에 해당한다면, .stage는 .house와 앞으로 추가될 캐릭터들이 놓이는 곳입니다.
.stage {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
transform-style: preserve-3d; //IE 지원하지 않음
}
이 .stage는 absolute로 띄워준 다음 화면 전체에 꽉차도록 했습니다.
그리고 transform-style:preserve-3d
속성을 줘서 3D 효과가 자신을 통과하도록 했죠.
참고로 이 속성은 IE에서 먹히지 않지만, IE 따위 이제 무시해야하지 않을까요? (쑻)
2. house 만들기
다음은 .house 차례입니다.
얘도 마찬가지로 전체 크기를 갖게 하고, preserve-3d를 줍니다.
.house {
position: relative;
width: 100vw;
height: 100vh;
transform-style: preserve-3d;
}
이제는 .house__wall 입니다. 얘들은 정면에서 주르륵 설 건데요, 마찬가지로 화면에 꽉 찬 크기를 갖습니다.
움직이게 하려면 absolute 속성을 줘야겠죠? 배경색도 하얗게 칠해줍니다.
작성하는 김에 .house__contents도 살짝 정리해줄게요.
&__wall {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
}
&__contents {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #333;
font-size: 5em;
}
그럼 위와 같이 벽들이 굉장히 부담스럽게 붙어있을 텐데요 허허
이걸 좀 떨어져서 바라봐야겠습니다.
그걸 위해 .house에게 transform: translateZ(-500vw)
를 줍니다.
다음은 각각의 벽을 스타일링합니다.
먼저 왼쪽 벽입니다.
왼쪽과 오른쪽의 두 벽은 길게 보여야하기 때문에, width:1000vw; 로 길게 만들어줍니다.
이 벽이 왼쪽에서 / <- 이 형태로 보여지게끔 Y축을 기준으로 회전시킵니다.
근데... 회전시켜보니 이따구(?)로 되어있어요.
그 이유는 크기가 1000vw이니, 회전축이 다른 house__wall 과 달랐기 때문이죠.
그럼 어떻게 해야할까요?
저 왼쪽벽의 중앙점을 바꿔버리면 되지 않을까요?
크기가 1000vw이니 그 절반의 500vw만큼 왼쪽으로 땡겨주는 거죠!
&--left {
left: -500vw;
}
또는 transform: translasteZ(-500vw) 해도 똑같은 결과를 얻을 수 있습니다 :-)
기억하세요, 크기의 절반!
&--left {
width: 1000vw;
transform: rotateY(90deg) translateZ(-500vw);
background: #f8f8f8;
}
오른쪽 벽도 왼쪽 벽과 비슷하게 만들어줍니다.
&--right {
width: 1000vw;
transform: rotateY(90deg) translateZ(-400vw);
background: #f8f8f8;
}
엇, 그런데 왜 얘는 -400vw일까요?
왜냐면 -500vw만큼 땡기면, 왼쪽 벽과 똑같은 모습이 될 테니까요.
얘는 오른쪽에 서야하니, 중앙벽 크기(100vw)만큼 덜 땡겨준 것입니다.
이제 .house__wall-front도 각각의 Z축 값을 지정해줍니다.
&--front {
&-a {
transform: translateZ(450vw);
}
&-b {
transform: translateZ(250vw);
}
&-c {
transform: translateZ(-100vw);
}
&-d {
transform: translateZ(-450vw);
}
}
3. 스크롤로 Z축 움직이기
다음은 스크롤로 house의 Z축을 움직여보겠습니다.
스크립트 파일을 작성해보죠! 충돌을 피하기 위해 즉시실행함수로 작성합니다.
(function () {
const houseElem = document.querySelector('.house');
function scrollHandler() {}
window.addEventListener('scroll', scrollHandler);
})()
앗, 그런데 잠깐!
지금 상태에서는 스크롤을 할 수가 없어요. .world가 position:fixed; 상태라 body에 높이값이 없거든요.
저희가 셀프로 임의의 높이값을 넣어줍니다.
body {
margin: 0;
padding: 0;
min-height: 100%;
height: 5000px; //스크롤 가능케 처리
background: #49b293;
-webkit-overflow-scrolling: touch;
}
자, 현재 .house가 transform: translateZ(-500vw);
상태입니다.
이제 이 벽들이 다가와야할 텐데요, 그러려면 Z값이 -500에서 점점 양수가 되게끔하면 될 거에요.
이제 우리에게 필요한 값은 무엇일까요?
예를 들어, 지금 전체 스크롤 높이가 대충... 3000이라고 합시다.
우리가 스크롤을 쭉 내려서 스크롤값이 2000이 됐습니다.
그럼 2000/3000=0.66만큼 스크롤한 게 됩니다.
이 값은 너무 작으니 1000 정도 곱하면, 660이 나옵니다.
즉, 최소 100부터 최대 1000까지의 값이 나오게 됩니다.
어, 그럼 이 값을 Z값으로 쓰면 되지 않을까요?
스크롤한값/전체스크롤*1000이란 수식이요!
좋아요! 그럼 스크롤한 값을 찾아내보죠!
우선 scrollHandler()
함수 안에다 콘솔로그를 작성합니다.
console.log(this.pageYOffset);
pageYOffset 값을 찍어보고 스크롤해볼게요.
음.. 보니까 스크롤이 제일 위에 있으면 0, 아래로 내릴 수록 값이 늘어나네요.
그런데 이 pageYOffset 값은 문서 전체의 높이인 5000이 되지 못합니다.
스크롤바의 상단 부분이 닿는 지점 = pageYOffset이라고 생각하면 됩니다.
그럼 '스크롤한값/전체스크롤*1000' 중에서 스크롤한값은 곧 this.pageYOffset이 됩니다.
전체스크롤 <- 이 값은 어떻게 구할까요? body의 높이는 아니죠. pageYOffset이 5000만큼 스크롤되지 못하니까요.
음... 그러면 결국, pageYOffset의 최대값을 구하면 되지 않을까요?
현재 body의 높이는 5000입니다.
하지만 pageYOffset의 최대값은 이보다 작고, 그 값도 윈도 높이에 따라 달라집니다.
지금 윈도 높이가 500이라고 하면, pageYOffset의 최대값은 얼마가 될까요?
콘솔에 찍어봤더니... 바로 4500이 됩니다. 5000에서 500을 뺀 값이죠!
윈도높이가 950이면? 5000-950=4050이 됩니다.
'스크롤한값/전체스크롤*1000' 중에서 전체스크롤은 (body높이 - window높이) 가 되겠네요!! (뜨든)
전체스크롤 <- 이 값을 maxScrollValue란 변수 안에 저장하겠습니다.
const maxScrollValue = document.body.offsetHeight - this.window.innerHeight;
(this.pageYOffset / maxScrollValue) * 1000
을 하면 되겠네요!
그런 다음 style.transform을 이용해 이 값만큼 Z축을 이동시킵니다.
function scrollHandler() {
let maxScrollValue = document.body.offsetHeight - this.window.innerHeight;
const zMove = (this.pageYOffset / maxScrollValue) * 1000;
houseElem.style.transform = 'translateZ(' + zMove + 'vw)';
}
그런 다음 스크롤 해보면...
오!!! 뭔가 움직여요!!!
움직이는데...!! 이상하네요. ⊙△⊙
스크롤하자마자 시점이 뿅하고 움직이거든요;;
사실 당연합니다.
원래 .house의 Z축은 얼마였죠? -500vw입니다.
그런데 저희가 만든 zMove의 값은 최소 0부터 최대 1000이 됩니다.
그래서 스크롤하자마자 Z축이 0으로 이동하면서 어색해지는 거죠.
방법은? -500부터 시작하도록 500을 빼주면 되죠!
const zMove = (this.pageYOffset / maxScrollValue) * 1000 - 500;
그럼 -500~500의 zMove 값에 따라 화면이 움직입니다.
그런데 최대값이 500이 되니까 마지막 화면이 너무 가깝게 붙네요.
이럴 때는 1000보다 작은 값을 곱하면 될 거 같습니다. 한 950 정도 줘보면, -500~450의 수치를 갖게 됩니다.
const zMove = (this.pageYOffset / maxScrollValue) * 950 - 500;
결국 코드는 요로콤 됩니다.
function scrollHandler() {
let maxScrollValue;
const zMove = (this.pageYOffset / maxScrollValue) * 950 - 500;
houseElem.style.transform = 'translateZ(' + zMove + 'vw)';
}
아, 한 가지 더 추가할 게 있어요.
바로 reszie 시의 처리입니다.
window.resize가 되면 zMove값이 바뀌어버릴 테니까요.
maxScrollValue는 밖으로 빼주고, resize에 대한 이벤트 리스너도 작성합니다.
let maxScrollValue;
function resizeHandler() {
maxScrollValue = document.body.offsetHeight - this.window.innerHeight;
}
window.addEventListener('resize', resizeHandler);
그럼 최초 로드 시 maxScrollValue값이 없을 텐데요,
방법은 간단합니다. 최초 로드 시에도 resizeHanlder()
를 호출해주세요 :>
let maxScrollValue;
function scrollHandler() {
const scrollPer = (this.pageYOffset / maxScrollValue);
const zMove = scrollPer * 950 - 500;
houseElem.style.transform = 'translateZ(' + zMove + 'vw)';
}
function resizeHandler() {
maxScrollValue = document.body.offsetHeight - this.window.innerHeight;
}
window.addEventListener('scroll', scrollHandler);
window.addEventListener('resize', resizeHandler);
resizeHandler();
4. 진행바 움직이기
이제 상단에 진행바를 만들어볼게요!
스크롤 정도에 따라 이 바의 길이가 늘어났다 줄어들었다 하게끔 만들어봅시다.
우선 마크업!!
<div class="progress">
<div class="progress__bar"></div>
</div>
참 쉽죠?
이어서 CSS!!
.progress {
z-index: 100;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 5px;
background: #555;
&__bar {
width: 0%;
height: 100%;
background: #00afff;
}
}
다음은 스크립트 차례입니다.
현재 window가 scroll 되었을 때 이벤트리스너가 하나 걸려있죠.
scrollHandler()
<- 요 안에다가 작성해주면 되겠습니다.
자! 이제 우리가 할 일은 JS로 저 &__bar의 width값을 조정하는 것입니다.
50%만큼 스크롤하면 width:50%이 되면 되죠.
그럼 저 50%만큼 스크롤했는지는 어떻게 아냐구요?
우리가 아까 썼던 코드를 떠올려보세요.
(스크롤한값/전체스크롤값)*100이 되겠죠?
그리고 그건 (this.pageYOffset / maxScrollValue) * 100
이고요!
앗, 이 값은 아까 구했던 거네요.
아묻따 변수에 넣어줍니다.
const scrollPer = (this.pageYOffset / maxScrollValue);
barElem.style.width = scrollPer * 100 + "%";
그리고 기존 코드 중에서 겹치는 부분은 변수로 대체해줍니다.
코드는 이렇게 정리되겠네요.
let maxScrollValue;
function scrollHandler() {
const scrollPer = (this.pageYOffset / maxScrollValue);
const zMove = scrollPer * 950 - 500;
houseElem.style.transform = 'translateZ(' + zMove + 'vw)';
barElem.style.width = scrollPer * 100 + "%";
}
5. 마우스 움직임에 따라 시점 변경하기
다음은 마우스 위치에 따라 화면의 시점을 변경해봅시다.
첫 번째로 mousemoveHandler를 만들어주세요.
인자로 이벤트 객체를 넘겨받고, e.clientX와 e.clientY를 콘솔에 찍어봅시다.
function mousemoveHandler(e) {
console.log(e.clientX, e.clientY);
}
window.addEventListener('mousemove', mousemoveHandler);
콘솔에 찍히는 값을 확인해보세요.
왼쪽 상단은 '0, 0'이고, 오른쪽 하단은 'window 너비, window 높이'가 됩니다.
이걸 어떻게 활용하냐구요? .stage를 회전할 때 필요한 값입니다.
음... 감이 잘 안 오니까 .stage를 직접 transform:rotate() 시켜보겠습니다.
.stage {transform: rotateX(20deg);}
오!! 뭔가 아래에서 위를 쳐다보는 듯한 느낌이 됐습니다.
이번에는 음수값을 줘볼까요?
위에서 아래를 쳐다보는 듯한 느낌이 됐습니다.
rotateY도 적용해보겠습니다.
그런데 우리는 이렇게까지 극적인 효과를 줄 필요는 없습니다.
-10~10 정도의 값만 주면 될 것 같아요. 그보다 작아도 되겠네요.
그럼 e.clientX, e.clientY값을 가지고 어떻게 그 목표값을 도출해낼 수 있을까요?
여기에 너비 100 짜리 window가 있습니다.
마우스를 왼쪽 끝으로 가져다대면 e.clientX는 0이겠죠?
정가운데 있으면 너비의 절반인 50이 되고, 오른쪽 끝에 있으면 너비 전체인 100이 될 것입니다.
그 아래에는 우리에게 필요한 목표값을 적어봤습니다.
-10~10 정도라 했는데 이건 너무 값이 크니 -1~1로 줄여서 생각해봅시다.
왜 왼쪽이 음수값이냐- 하면, 아까 임의로 rotateX(-20deg)를 줬을 때를 생각해보세요.
마치 왼쪽에서 바라보고 있는 듯한 느낌이 들었죠?
양수인 rotateX(20deg)를 줬을 때는 오른쪽에서 바라보는 느낌이 들었고요.
우리는 마우스가 위치한 지점에서 바라보고 있는 듯한 느낌을 줘야하기 때문에 -1, 0, 1의 값이 필요합니다.
가운데가 0인 이유는? 마우스가 가운데 있을 때는 화면이 가만히 있어야 하니까요ㅎ_ㅎ
어떤 수식을 써야할지 막막한데...@.@
우선은 비율을 계산해봅시다.
먼저, 마우스가 정가운데 있을 때 상황입니다.
e.clientX / window.innerWidth
를 하면, 50 / 100 = 0.5
가 나옵니다.
여기에다 100을 곱하면 50%라는 비율이 나오겠죠?
그런데 100 대신 2를 곱해봅시다. 그럼 정수인 1이란 값이 나옵니다.
이번엔 가장 왼쪽에 있을 때입니다.
(e.clientX / window.innerWidth * 2) = (0 / 500 * 2) = 0
이네요.
가장 오른쪽이라면?
e.clientX / window.innerWidth * 2) = (500 / 500 * 2) = 2
입니다.
결국 0, 1, 2라는 값이 나오네요.
어라? 저희가 찾던 -1, 0, 1과 거의 근접해있지 않나요?
저 값들에서 딱 1만 빼주면 되잖아요?!
그래서 수식은
(e.clientX / window.innerWidth * 2) - 1
이 되겠습니다 :>
한번 코드에 넣어 실험해봅시다!
function mousemoveHandler(e) {
const x = (e.clientX / window.innerWidth * 2) - 1;
stageElem.style.transform = 'rotateY(' + x * 10 + 'deg)';
}
window.addEventListener('mousemove', mousemoveHandler);
크으, 잘 돌아가네요~
이어서 X축도 작성해보겠습니다.
우선 감을 잡기 위해 임의로 rotateY를 시켜볼게요.
그럼 우리가 구해야할 값이 명확해지네요.
위에서부터 순서대로 1, 0, -1 이 됩니다.
아까의 수식을 응용해서 (e.clientY / window.innerHeight)*2
마우스가 맨 위라면 (0/100)*2 = 0,
마우스가 가운데라면 (50/100)*2 = 1,
마우스가 맨 아래라면 (100/100)* = 2,
라는 값이 나오네요
[0, 1, 2]를 [1,0,-1]로 만드려면?
1에서 그 값을 빼주면 됩니다.
1 - 0 = 1
1 - 1 = 0
1 - 2 = -1
이 나오게 되죠!
그래서 수식은 1 - (e.clientY / window.innerHeight * 2)
가 됩니다.
function mousemoveHandler(e) {
const y = 1 - (e.clientX / window.innerHeight * 2);
stageElem.style.transform = 'rotateX(' + y * 10 + 'deg)';
}
window.addEventListener('mousemove', mousemoveHandler);
이제 코드를 좀 더 다듬어 보겠습니다.
x축과 y축이 동시에 움직이게 해보죠!
const mousePos = { x: 0, y: 0 };
function mousemoveHandler(e) {
mousePos.x = -1 + (e.clientX / window.innerWidth * 2);
mousePos.y = 1 - (e.clientY / window.innerHeight * 2);
stageElem.style.transform = 'rotateY(' + (mousePos.x * 5) + 'deg) rotateX(' + (mousePos.y * 5) + 'deg)';
}
마우스를 움직이면 각도가 쓱쓱 바뀌는 걸 볼 수 있습니다 :D
6. 캐릭터 구현하기
다음은 클릭할 때마다 캐릭터가 뿅뿅 튀어나오게 할 차례입니다.
물론 이건 JS로 처리할 거지만, 우선 확인을 위해 html 상에 마크업을 해줍니다.
캐릭터는 크게 '머리 + 몸 + 오른팔 + 왼팔 + 오른다리 + 왼다리'의 6개로 이루어져 있습니다.
그리고 그 부위(?) 안에는 앞면과 뒷면이 존재하죠.
구조를 나타내면 이렇게 됩니다.
character > head > front + back
> torso > front + back
> arm-right > front + back
> arm-left > front + back
> leg-right > front + back
> leg-left > front + back
위 구조를 기반으로 .head를 마크업하면 아래와 같습니다.
<div class="char__con char__head">
<div class="char__face char__head-face face-front"></div>
<div class="char__face char__head-face face-back"></div>
</div>
그럼 나머지 부위ㅋㅋㅋㅋ들도 이런 식으로 작성하면 되겠습니다.
<div class="char">
<div class="char__con char__head">
<div class="char__face char__head-face face-front"></div>
<div class="char__face char__head-face face-back"></div>
</div>
<div class="char__con char__torso">
<div class="char__face char__torso-face face-front"></div>
<div class="char__face char__torso-face face-back"></div>
</div>
<div class="char__con char__arm char__armRight">
<div class="char__face char__arm-face face-front"></div>
<div class="char__face char__arm-face face-back"></div>
</div>
<div class="char__con char__arm char__armLeft">
<div class="char__face char__arm-face face-front"></div>
<div class="char__face char__arm-face face-back"></div>
</div>
<div class="char__con char__leg char__legRight">
<div class="char__face char__leg-face face-front"></div>
<div class="char__face char__leg-face face-back"></div>
</div>
<div class="char__con char__leg char__legLeft">
<div class="char__face char__leg-face face-front"></div>
<div class="char__face char__leg-face face-back"></div>
</div>
</div>
CSS는 어떻게 작성하면 될까요?
우선 전체를 감싸고 있는 .char을 스타일링 해보죠!
.char {
position: absolute;
left: 12%;
bottom: 5%;
width: 10vw;
height: 15.58vw;
transform-style: preserve-3d;
}
화면상에서 자유롭게 움직여야하니 absolute 속성을 주었고,
초기 위치는 left:12%; bottom:5%;
입니다.
한편 크기값은 width:10vw; height:15.58vw;
인데요. 이건 곧 '1 : 1.558' 비율임을 뜻합니다.
예를 들어 캐릭터 이미지 전체의 크기가 1000px * 1558px 이라면 이게 1:1.558이 됩니다.
이렇게 크기값을 vw로 준 이유는 창 크기에 따라 유동적으로 캐릭터 크기가 바뀌게 하기 위함입니다.
또, 상위 엘리먼트에 적용된 3d 효과가 자신을 잘 통과할 수 있도록 preserve-3d 속성도 줍니다.
.char[data-direction='forward'] {}
.char[data-direction='backward'] {}
.char[data-direction='left'] {}
.char[data-direction='right'] {}
위 속성들은 캐릭터 방향에 따라 캐릭터를 회전시키는 데 사용할 예정입니다.
data- 속성을 활용할 거니까, JS랑 짝짜꿍해서 처리하면 되겠죠?!
.char__con {
position: absolute;
transform-style: preserve-3d;
transition: 1s;
}
그리고 각 부위를 나타내는 .char__con도 absolute로 띄어줍니다.
훗날(?)을 위해 transition 속성도 살짜쿵 넣어주고요.
.char__face {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-size: cover;
backface-visibility: hidden;
&.face-back {
transform: rotateY(180deg);
}
}
.char__face는 각 부위 안에 2개씩 존재하는 면(face)입니다.
앞뒤로 배치하려면 마찬가지로 absolute 시켜야겠죠?
크기는 부모를 따라가도록 하고, 백그라운드 이미지 사이즈도 지정합니다.
(어차피 여기선 크기를 지정해놔서 cover든 contain이든 무관해요!)
backfave-visibility: hidden; 속성도 꼭 넣어줍시다. 안 그럼 뒤집혀졌을 때 뒷면 때문에 이상하게 보일 거에요.
뒤집혔을 때의 모습은 .face-back이라는 클래스로 제어하기로 합니다.
.char__head { //머리 부분
top: 0;
left: calc(42 / 856 * 100%);
}
이제 머리 부분을 작성해보겠습니다.
머리의 left 위치값을 calc() 함수로 작성했는데요, 이는 비율에 따라 유동적으로 바뀌기 때문입니다.
위 이미지처럼, 전체 너비가 856px일 때 왼쪽에서부터 42px만큼 떨어진 상태인 거죠.
그래서 42/856을 한 다음 100%를 곱해서 퍼센트값을 가져왔습니다.
.char__head {
top: 0;
left: calc(42 / 856 * 100%);
width: calc(770 / 856 * 100%);
height: calc(648 / 1334 * 100%);
}
width, height도 마찬가지로 그 비율대로 가져왔습니다 :>
이제 나머지 속성들도 추가해줍니다.
.char__head {
z-index: 60;
top: 0;
left: calc(42 / 856 * 100%);
width: calc(770 / 856 * 100%);
height: calc(648 / 1334 * 100%);
transform-origin: center bottom;
animation: ani-head 0.6s infinite alternate cubic-bezier(0.46, 0.18, 0.66, 0.93);
}
이 캐릭터는 이따가 머리를 까딱까딱할 거라서 transform-origin을 가운데 아래로 지정해주었습니다ㅎㅎ
그리고 그 까딱까딱 모션은 ani-head란 이름으로 추후 작성하려고 합니다.
.char__head {
//머리 부분
z-index: 60;
top: 0;
left: calc(42 / 856 * 100%);
width: calc(770 / 856 * 100%);
height: calc(648 / 1334 * 100%);
transform-origin: center bottom;
animation: ani-head 0.6s infinite alternate cubic-bezier(0.46, 0.18, 0.66, 0.93);
&-face {
//머리의 각 면
&.face-front {
@include bgi("ilbuni_head_front");
}
&.face-back {
@include bgi("ilbuni_head_back");
}
}
}
그리고 앞, 뒤에 이미지도 쓱 넣어줍니다.
ㅋㅋㅋㅋㅋㅋ귀엽게 잘나오네요!
이어서 몸통이랑 두 팔다리도 추가해줍니다.
.char__torso {
//몸통 부분
z-index: 50;
left: calc(208 / 856 * 100%);
top: calc(647 / 1334 * 100%);
width: calc(428 / 856 * 100%);
height: calc(385 / 1334 * 100%);
transform-origin: center center;
&-face {
//몸통의 각 양면
&.face-front {
@include bgi("ilbuni_body_front");
}
&.face-back {
@include bgi("ilbuni_body_back");
}
}
}
.char__armRight {
top: calc(648 / 1334 * 100%);
left: 0;
width: calc(244 / 856 * 100%);
height: calc(307 / 1334 * 100%);
transform-origin: right top;
.face-front {
@include bgi("ilbuni_arm_0");
}
.face-back {
@include bgi("ilbuni_arm_1");
}
}
.char__armLeft {
top: calc(648 / 1334 * 100%);
left: calc(600 / 856 * 100%);
width: calc(244 / 856 * 100%);
height: calc(307 / 1334 * 100%);
transform-origin: left top;
.face-front {
@include bgi("ilbuni_arm_1");
}
.face-back {
@include bgi("ilbuni_arm_0");
}
}
.char__legRight {
top: calc(1031 / 1334 * 100%);
left: calc(200 / 856 * 100%);
width: calc(230 / 856 * 100%);
height: calc(300 / 1334 * 100%);
transform-origin: center top;
.face-front {
@include bgi("ilbuni_leg_0");
}
.face-back {
@include bgi("ilbuni_leg_1");
}
}
.char__legLeft {
top: calc(1031 / 1334 * 100%);
left: calc(414 / 856 * 100%);
width: calc(230 / 856 * 100%);
height: calc(300 / 1334 * 100%);
transform-origin: center top;
.face-front {
@include bgi("ilbuni_leg_1");
}
.face-back {
@include bgi("ilbuni_leg_0");
}
}
.char[data-direction='forward'] {
transform: rotateY(180deg);
}
.char[data-direction='backward'] {
transform: rotateY(0deg);
}
.char[data-direction='left'] {
transform: rotateY(-90deg);
}
.char[data-direction='right'] {
transform: rotateY(90deg);
}
아까 작성하다만 data- 부분도 적어줍니다.
캐릭터가 현재 뒤(우리쪽)을 보고 있는 상태이니 backward 상태겠죠?
앞(벽쪽)을 보고 있는 상태라면 Y축으로 180도만큼 뒤집어주면 될 거고요.
CSS 작성이 끝나면 확인을 위해 마크업 상에 data-를 추가해보겠습니다.
<div class="char" data-direction="right">
...
</div>
잘 나오네요! 👐
이제 캐릭터에게 애니메이션 효과를 주면 되겠습니다.
1) 머리 까딱까딱 모션
2) 달리는 모션
이렇게 두 가지가 필요합니다.
.char.running {
.char__armRight {
animation: ani-running-arm 0.2s alternate infinite linear;
}
.char__armLeft {
animation: ani-running-arm 0.2s alternate-reverse infinite linear;
}
.char__legRight {
animation: ani-running-leg 0.2s alternate infinite linear;
}
.char__legLeft {
animation: ani-running-leg 0.2s alternate-reverse infinite linear;
}
}
따라서 머리에게는 ani-head 애니메이션을,
달리는 동안의 팔다리에게는 ani-running 애니메이션을 줍니다.
왔다갔다하며 계속 반복되어야하니 alternate와 inifinite 속성을 잊지마세요!
또, 왼쪽 팔다리에게는 alternate-reverse 속성을 줍니다.
우리가 걸을 땐 팔다리가 같은 방향을 향하지 않고 서로 다른 방향을 향해 교차하니까요.
@keyframes ani-head {
100% {
transform: rotateX(-10deg);
}
}
@keyframes ani-running-leg {
0% {
transform: rotateX(-30deg);
}
100% {
transform: rotateX(30deg);
}
}
@keyframes ani-running-arm {
0% {
transform: rotateX(30deg);
}
100% {
transform: rotateX(-30deg);
}
}
귀엽네요
😊
7. 자바스크립트로 캐릭터 생성하기
지금까지는 캐릭터를 쌩 HTML로 작성했는데,
이걸 스크립트를 통해 뿅뿅 만들어지게끔 처리할게요.
우선 기존 마크업은 지워줍니다.
그리고 character.js 파일을 만듭니다.
function Character() {
//여기서 캐릭터 생성
}
그리고 html에도 연결해줘야죠.
<script src="js/character.js"></script>
<script src="js/script.js"></script>
이제 script.js 파일에서 Character 생성자 함수를 사용할 수 있습니다.
바로 이렇게 new 키워드를 쓰면 됩니다.
생성자니까 대문자로 시작하는 것도 살짝 체크해줍니다.
new Character();
자, 그럼 요 Character 함수 내에 우리가 아까 만든 html을 넣으면 될 텐데요.
그냥 변수에 작성하는 게 아니라 this를 사용합니다.
function Character() {
this.mainElem = document.createElement("div");
}
이 생성자를 통해 만들어진 인스턴트 객체의 속성으로 쓸 거니까 this를 사용하는 거죠!
빈 div를 만들고 클래스네임도 부여합니다.
function Character() {
this.mainElem = document.createElement("div");
this.mainElem.classList.add("char");
this.mainElem.innerHTML = "";
}
마지막으로 innerHTML을 통해 내용을 채워넣습니다.
고전적인 방식으로 가면 + 연산자로 하나하나 연결해줄 수 있는데,
귀찮으니까 `` 안에 쓱 넣어줍니다.
function Character() {
this.mainElem = document.createElement("div");
this.mainElem.classList.add("char");
this.mainElem.innerHTML = `
<div class="char running" data-direction="back">
<div class="char__con char__head">
<div class="char__face char__head-face face-front"></div>
<div class="char__face char__head-face face-back"></div>
</div>
<div class="char__con char__torso">
<div class="char__face char__torso-face face-front"></div>
<div class="char__face char__torso-face face-back"></div>
</div>
<div class="char__con char__arm char__armRight">
<div class="char__face char__arm-face face-front"></div>
<div class="char__face char__arm-face face-back"></div>
</div>
<div class="char__con char__arm char__armLeft">
<div class="char__face char__arm-face face-front"></div>
<div class="char__face char__arm-face face-back"></div>
</div>
<div class="char__con char__leg char__legRight">
<div class="char__face char__leg-face face-front"></div>
<div class="char__face char__leg-face face-back"></div>
</div>
<div class="char__con char__leg char__legLeft">
<div class="char__face char__leg-face face-front"></div>
<div class="char__face char__leg-face face-back"></div>
</div>
</div>
`;
}
자, 이 캐릭터를 이제 html 에 넣어줘야하는데요, 들어가야하는 위치는 stage 아래입니다.
그럼 stage.appendChild 처럼 쓰면 되겠네요!
이때 this <- 붙여주는 거 까먹지 맙시다(to me).
document.querySelector(".stage").appendChild(this.mainElem);
8. 마우스 클릭으로 캐릭터 소환하기
지금은 페이지가 로드되자마자 캐릭터가 보일 텐데, 마우스 클릭으로만 나타나게 하겠습니다.
클릭된 위치를 파악해서 그 지점에 뙇 소환하는 거죠.
stageElem에게 이벤트리스너를 걸어줍니다.
stageElem.addEventListener("click", function (e) {
console.dir(e.clientX);
});
그럼 클릭된 지점의 X축을 알 수 있죠.
캐릭터의 기존 위치값은 left: 12% 였는데, 여기에 유동적인 퍼센트값을 넣어주면 되겠습니다.
어떻게요? e.clientX/window.innerWidth * 100 하면 되죠!
그리고 이 값을 Character 생성자 함수의 매개변수로 넣어 건네줄 생각입니다. 무슨 도시락 넣어주는 거 같네요ㅋㅋㅋㅋ
근데 그냥 달랑 넣어보내는 게 아니라...
stageElem.addEventListener("click", function (e) {
new Character({
xPos: (e.clientX / window.innerWidth) * 100
});
});
객체로 보낼 거에요!
왜냐면 xPos 값 말고도 여러가지 값을 보낼 거거든요.
(도시락이라고 한다면 밥만 싸주는 게 아니라 반찬도 싸주는 거죠ㅎㅎ)
이제 char.js로 돌아가 저 값을 받아줍니다.
function Character(info) {
....
this.mainElem.style.left = info.xPos + "%";
...
}
그럼 우리가 원하는 대로 클릭한 위치에 캐릭터가 뿅뿅 소환됩니다.
후, 길군요!
이 이후의 내용은 다음 포스트에서 만나요오오-