• [CSS] 3D Transform

    2019. 7. 1.

    by. 나나 (nykim)

    1. Perspective (투영점)

    1) perspective property

    보고 있는 사람의 위치를 추정하여 투영점을 명시하면 3D 환경을 만들 수 있습니다.

    말하자면 멀리 떨어진 사물은 작게, 가까이 있는 사물은 크게 만들어서 원근감을 주는 거죠.

     

    .scene--blue {
      /* perspective property */
      perspective: 400px;
    }
    
    .panel--blue {
      transform: rotateY(45deg);
    }

    See the Pen Perspective property by Dave DeSandro (@desandro) on CodePen.

     

     

     

     

    파란색 박스의 부모에게 perspective 속성을 주고, 박스에게는 transform을 시킨 모습입니다.

    여기에서의 400px은 '내가 얼마나 떨어져서 보고 있는가'를 나타낸다고 할 수 있는데요, 즉 400px만큼 떨어져서 보고 있는 셈입니다.

    따라서 이 값이 작으면 작을 수록 더 가까이에서 보는 것으로 처리됩니다. 그러니 원근감이 더 극적으로 나타나겠죠!

     

    위 이미지에서 파란색 원형은 3d 공간 상의 물체입니다.

    d는 보는 사람과 화면과의 거리이고(=perspective) Z는 z축상 요소의 위치를 말합니다.

    이미지에서도 보이다시피, d가 작을수록 동그라미가 더 극적으로 보이는 걸 알 수 있죠! (출처)

     

    그럼 이 상태에서 여러 개의 박스를 만들어보겠습니다.

     

    See the Pen Perspective property children by Dave DeSandro (@desandro) on CodePen.

     

     

     

     

     

    엌 뭔가 생각과는 다른 결과물이네요! 파란색 박스들이 저마다 다른 모습으로 변형됐습니다 ;ㅁ;

    이는 각 박스들이 저마다 다른 투영점(perspective), 다른 소실점(vanishing point)을 갖고 있기 때문입니다.

    (왜냐면 우린 perspective 속성을 부모에다 줬잖아요!)

     

    만약 박스들이 동일한 투영점을 갖게 하려면 어떻게 해야할까요?

    박스 각각에다 perspective 속성을 주면 되지 않을까요?

     

     

    2) transform: perspective

    개별적인 투영점을 설정할 때는 transform 속성의 값으로 perspetive를 주고, 괄호 안에 수치를 입력합니다.

     

    .panel--separate {
      transform: perspective(400px) rotateY(45deg);
    }

     

    See the Pen Perspective function children by Dave DeSandro (@desandro) on CodePen.

     

     

     

     

    3) perspective-origin

     

    perspective와 관련된 속성으로는 perspective-origin이 있습니다.

    이것도 마찬가지로 보는 사람이 어느 위치에서 보고 있는지를 나타내는 속성입니다.

    perspetive 속성과 함께 소실점을 나타내는 데 사용합니다.

     

    디폴트는 perspective-origin: 50% 50%;이며, 각각 x축 y축에 해당합니다.

    예제를 한 번 봅시다.

     

     

    '50% 50%'와 'top left'를 비교해보면 그 차이가 드러납니다.

    '50% 50%'는 정중앙에 서서 지켜보고 있는 느낌이죠! 소실점도 정 가운데고요.

    반면 'top left'는 왼쪽 아래에서 지켜보고 있는 느낌입니다. 소실점은 왼쪽 위네요.

     

    따라서 이 속성의 값은 소실점이 어디있는가를 나타낸다고 보면 되겠습니다.

     

    (대충 이런 느낌입니다. 진짜 대충...)

     

     

    아래 예제에서 슬라이더를 움직이다보면 사알짝 느낌이 올지도 몰라요(?!)

     

    See the Pen Perspective cube by Dave DeSandro (@desandro) on CodePen.

     

     

     

     


     

    2. Card Filp

     

    perspective 속성을 배웠으니 써먹을 차례입니다 ;)

    CSS를 활용해 카드 뒤집기 애니메이션을 만들어보겠습니다. (출처)

     

    우선 마크업을 해줍니다.

    <div class="scene">
      <div class="card">
        <div class="card__face card__face--front">front</div>
        <div class="card__face card__face--back">back</div>
      </div>
    </div>

     

    .scene은 3D 공간이 될 부모 요소이며, .card는 3D 오브젝트이고, .card__face는 카드의 겉면으로 사용될 것입니다.

    앞으로도 여러 3D transform을 다룰 텐데, 위와 같이 scene, object, faces의 패턴을 사용하는 것을 추천합니다.

     

    먼저 scene에게 perspective를 주겠습니다.

     

    .scene {
      width: 200px;
      height: 260px;
      perspective: 600px;
    }

     

    이제 .card는 부모의 3D 공간 속에서 자유롭게 변형될 수 있습니다.

    .card에게는 width와 height를 100%씩 주어서 transform-origin이 정중앙에서 일어나도록 합니다.

    .card__face는 absolute시킬 거니까, .card는 relative 속성을 줍니다.

     

    .card {
      width: 100%;
      height: 100%;
      position: relative;
      transition: transform 1s;
      transform-style: preserve-3d;
    }

     

    근데 못보던 속성이 있네요! preserve-3d가 뭐죠?

     

    자, 요소의 perspective 속성은 바로 아래의 자식 요소에게만 적용됩니다. 

    우리는 perspective를 scene에게 주었으니까 오로지 .card만 3D transform이 가능한 상태였던 거죠.

    transform-style: preserve-3d는 이 perspective를 부모로부터 받아 스윽 자신을 통과해 자식까지 전달되도록 합니다.

    이게 없다면 .card__face는 아무런 3D 효과도 줄 수가 없어요!

    참고로 이 preserve-3d는 IE에서 지원하지 않습니다. (하지만 IE따위 스루합시다)

     

    그 다음은 .card__face를 스타일링합니다.

    둘이 겹쳐져야 하니까 absolute 시켜야겠죠?

     

    .card__face {
      position: absolute;
      width: 100%;
      height: 100%;
      backface-visibility: hidden;	
    	
      &--front {
        background-color: Tomato;
      }
    	
      &--back {
        background-color: SteelBlue;
        transform: rotateY(180deg);
      }
    }

     

    backface-visibility: hidden는 3D Transform에서 요소의 뒷면을 숨기는 역할을 합니다.

    이걸 hidden 처리하지 않으면 앞면/뒷면이 함께 보이기 때문에 번쩍거리면서 이상하게 나오거든요.

     

    또, 앞면은 빨간색 뒷면은 파란색을 주고, 뒷면은 Y축으로 180만큼 회전시켰습니다.

    마지막으로 뒤집혔을 때의 모습을 만들어야겠죠!

     

    .card.is-flipped {
       transform: rotateY(180deg);
    }

     

    그 다음 클래스를 토글하기만 하면 끝!

     

    var card = document.querySelector('.card');
    card.addEventListener( 'click', function() {
      card.classList.toggle('is-flipped');
    });

     

    See the Pen rEdNeR by NY KIM (@nykim_) on CodePen.

     

     

     

     

     


     

    3. 3D Cube

     

    이번에는 평면이 아닌 입체를 만들어봅니다.

    천장, 바닥, 벽이 존재하는 큐브를 CSS로 그려봐요!

     

    아까 scene - object - face 의 패턴을 사용하는 게 좋다고 했었죠.

    이번에도 그에 맞게 마크업합니다. 큐브가 되기 위해서는 총 6가지의 face를 추가해야 합니다.

     

    <div class="scene">
      <div class="cube">
        <div class="cube__face cube__face--front">front</div>
        <div class="cube__face cube__face--back">back</div>
        <div class="cube__face cube__face--right">right</div>
        <div class="cube__face cube__face--left">left</div>
        <div class="cube__face cube__face--top">top</div>
        <div class="cube__face cube__face--bottom">bottom</div>
      </div>
    </div>

     

    CSS도 예쁘게 넣어줍니다.

     

    .scene { 
       width: 200px;
       height: 200px;
       perspective: 600px;
    }
    
    .cube { 
       position: relative;
       width: 100%;
       height: 100%;
       transform-style: preserve-3d;
    }
    
    .cube__face {
       position: absolute;
       width: 100%;
       height: 100%;
    }
    
    

     

    이제 앞면을 제외한 면들은 제자리로 가야겠죠? rotate 시켜줍니다.

     

    .cube__face--front  { transform: rotateY(  0deg); }
    .cube__face--right  { transform: rotateY( 90deg); }
    .cube__face--back   { transform: rotateY(180deg); }
    .cube__face--left   { transform: rotateY(-90deg); }
    .cube__face--top    { transform: rotateX( 90deg); }
    .cube__face--bottom { transform: rotateX(-90deg); }

     

     

    그런데 지금 시점에서는 아무것도 안 보입니다.

    왜냐하면 다들 수직으로 있기 때문에 거의 안보이는 거나 마찬가지거든요.

    따라서 얘네가 보일 수 있게 이동시켜줘야 합니다.

     

    각 면들은 현재 중앙에 위치해있기 때문에, 면 크기(200px)의 절반만큼(100px) 이동시켜줍니다.

    아니면 absolute 되어 있으니 top 또는 left 값을 100px로 해도 되고요.

    그런데 이왕 transform 쓴 거, translateZ 값으로 옮겨봅시다. 이때, 앞뒷면도 값을 주는 거 잊지 말자구요!

     

    .cube__face--front  { transform: rotateY(  0deg) translateZ(100px); }
    .cube__face--right  { transform: rotateY( 90deg) translateZ(100px); }
    .cube__face--back   { transform: rotateY(180deg) translateZ(100px); }
    .cube__face--left   { transform: rotateY(-90deg) translateZ(100px); }
    .cube__face--top    { transform: rotateX( 90deg) translateZ(100px); }
    .cube__face--bottom { transform: rotateX(-90deg) translateZ(100px); }

     

    그럴싸한 모양이 갖춰졌습니다!

    그런데 잘 보니까 좀 이상하네요. 큐브 크기가 우리가 설정했던 200px보다 큽니다;; 글자도 희미하게 렌더링 되고요.

     

    그건 우리가 면들을 translateZ 시키면서 음... 가까이 다가왔기(?) 때문인데요,

    이 면들을 감싼 object, 그러니까 .cube를 100px만큼 뒤축으로 밀면 됩니다.

     

    3D 변형을 적용할 때, 브라우저는 요소의 스냅샷을 찍은 다음 그 픽셀들을 재 렌더링합니다. 그래서 폰트는 변형된 사이즈에 맞게 안티 앨리어싱이 되지 않는다고 합니다. (3D transforms affect text rendering. When you apply a 3D transform, browsers take a snap-shot of the element and then re-render those pixels with 3D transforms applied. As such, fonts don’t have the same anti-aliasing given their transformed size.)

     

    .cube { transform: translateZ(-100px); }

     

     

    그럼 이제 완성입니다!

    요 .cube를 rotate시키면서 우리가 만든 큐브에 감탄해보자구요 (*´▽`*)

     

    See the Pen Cube by NY KIM (@nykim_) on CodePen.

     

     

     

     


     

    4. Box

     

    큐브 구현까지는 쉬었습니다-만, 입체도형의 형태가 정사각형이 아니라면 어떨까요?!

    이번에는 일반적이지 않은 직사각형 프리즘(a non-regular rectangular prism)을, 그러니까, 어... 박스를 만들어보겠습니다.

    너비 300px, 높이 200px, 깊이 100px차리 택배박스를 만든다고 생각합시다.

     

    scene - object - face 패턴에 입각해 마크업을 합니다.

     

    <div class="scene">
      <div class="box">
        <div class="box__face box__face--front">front</div>
        <div class="box__face box__face--back">back</div>
        <div class="box__face box__face--right">right</div>
        <div class="box__face box__face--left">left</div>
        <div class="box__face box__face--top">top</div>
        <div class="box__face box__face--bottom">bottom</div>
      </div>
    </div>

     

    각 면의 역할에 맞게 크기를 지정해주고, 크기 지정이 끝나면 absolute 시켜줍니다.

     

     

     

    자, 우선 Left 벽면부터 움직입니다. 기본적으로 이 상태일 텐데요!

     

    현재 상태

     

    left: 100px 값을 줘서 중앙으로 오도록 합니다.

    정사각형의 큐브일 때는 각자 중앙에 위치해 있었지만 지금은 직사각형이니 우리가 손수 옮겨줘야 하죠.

     

    left: 100px로 가운데로 옮겨줍니다

     

    rotateY로 90도만큼 회전시켜주고,

    Front 벽면의 너비가 300px이니까 그 절반인 150px만큼 translateZ로 이동시켜줍니다.

    .box__face {
       &--left {
          left: 100px;
          transform: rotateY(-90deg) translateZ(150px);
       }
    }

     

    90도로 꺾어주고(?) Z축을 이동시킵니다

     

     

    다음은 오른쪽 벽면입니다. 얘도 마찬가지로 left: 100px를 이용해 중앙으로 붙여준 다음, 회전과 이동을 해줍니다.

     

    .box__face {
       &--right {
          left: 100px;
          transform: rotateY(90deg) translateZ(150px);
       }
    }

     

     

    그 다음은 천장입니다.

     

    이 천장을...

     

    이렇게 위에 있는 녀석을 또 중앙으로 옮겨줘야 하네요.

    높이가 200px인데, top face의 높이는 100px입니다. 그럼 top:50px만큼 주면 되겠죠?

     

    가운데로 슝-

     

    그리고 rotateX(90deg)로 꺾어준 다음,

    높이값(200px)의 절반(100px)만큼 translateZ로 밀어줍니다.

     

    .box__face {
       &--top {
          top: 50px;
          transform: rotateX(90deg) translateZ(100px);
       }
    }

     

    꺾고 보내버리기!

     

    바닥도 마찬가지로 CSS를 작성합니다. 천장이랑은 rotateX의 각도만 다르면 되죠!

     

    .box__face {
       &--bottom {
          top: 50px;
          transform: rotateX(-90deg) translateZ(100px);
       }
    }

    바닥도 시공 완료!

     

    마지막은 앞뒷면입니다. 얘네는 이미 중앙에 있으니 굳이 옮겨줄 필요가 없어요.

    천장과 바닥 높이값(100px)의 절반만큼 translateZ(50px) 해주면 됩니다.

     

    .box__face {
       &--front {
          transform: rotateY(0deg) translateZ(50px);
       }
       &--back {
          transform: rotateY(180deg) translateZ(50px);
       }
    }

     

    짠, 직사각형 큐브를 만들었습니다 ;-)

     

    앗, 그런데 잊지 말아야할 것!

    지금 사물은 보이는 것보다 가까이에 있습니다 (?)

    다가온 만큼 translateZ로 밀어줘야하죠. .box에게 translateZ(-50px)을 줍니다.

     

    See the Pen BgrGGr by NY KIM (@nykim_) on CodePen.

     

    댓글 0