-
320x100
+ 이 글의 내용은 인프런의 [인터랙티브 웹 개발 제대로 시작하기] 강좌의 '종합예제' 부분 내용을 담고 있습니다.
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 + "%"; ... }
그럼 우리가 원하는 대로 클릭한 위치에 캐릭터가 뿅뿅 소환됩니다.
후, 길군요!
이 이후의 내용은 다음 포스트에서 만나요오오-
728x90'Blog > CSS' 카테고리의 다른 글
반응형으로 비디오(유튜브) 삽입하기 (15) 2019.10.31 [CSS 3D] 인터랙티브 웹 효과 구현하기 (2) (2) 2019.08.12 [SASS] node-sass 사용하기 (0) 2019.08.01 [CSS] 3D Transform (5) 2019.07.01 [CSS] 레이아웃을 더 편하게, Flexbox (0) 2019.01.07 댓글