Blog/CSS

[CSS 3D] 인터랙티브 웹 효과 구현하기 (1)

나나 (nykim) 2019. 8. 5. 19:08
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);}

 

rotateX(20deg)

 

 

오!! 뭔가 아래에서 위를 쳐다보는 듯한 느낌이 됐습니다.

이번에는 음수값을 줘볼까요?

 

 

rotateX(-20deg)

 

위에서 아래를 쳐다보는 듯한 느낌이 됐습니다.

rotateY도 적용해보겠습니다.

 

 

rotateY(20deg)

 

rotateY(-20deg)

 

그런데 우리는 이렇게까지 극적인 효과를 줄 필요는 없습니다.

-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를 시켜볼게요.

 

rotateX(20deg) & rotateX(-20deg)

 

그럼 우리가 구해야할 값이 명확해지네요.

위에서부터 순서대로 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