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

    2019. 8. 5.

    by. 나나 (nykim)

    + 이 글의 내용은 인프런의 [인터랙티브 웹 개발 제대로 시작하기] 강좌의 '종합예제' 부분 내용을 담고 있습니다.

     

     

    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 + "%";
      ...
    }

     

    그럼 우리가 원하는 대로 클릭한 위치에 캐릭터가 뿅뿅 소환됩니다.

     

     

     

    후, 길군요!

    이 이후의 내용은 다음 포스트에서 만나요오오-

    댓글 0