# 3D Transforms (optional)

NEED-TO-KNOW

  • Only 2D transforms are the subject matter to be mastered for this course
  • 3D transforms are NOT PART OF THE SUBJECT MATTER to be mastered for this course, but they may challenge you or inspire you for future projects

REMARKS

  • The content on this page requires quite a bit of spatial insight
  • In order to demonstrate some 3D properties and methods, we make use of transitions and animations
  • The 2D and 3D transform methods and transform-related properties are:
2D and 3D 3D only
translate(unit ,[unit]),translateX(unit), translateY(unit) translateZ(unit), translate3d(tX, tY, tZ)
rotate(angle), rotateX(angle), rotateY(angle) rotateZ(angle), translate3d(nr, nr, nr, angle)
scale(nr [, nr]), scaleX(nr), scaleY(nr) scaleZ(nr), scale3d(sX, sY, sZ)
skew(angle), skewX(angle), skewY(angle)
transform-origin: unit, unit perspective-origin: unit, unit
backface-visibility: visible | hidden
perspective(unit) (on element itself)
perspective: unit (on parent element)
transform-style: flat | preserve-3d

# translateZ() and translate3d()

  • translateZ(): moves an element on the Z axis
    • Positive arguments brings the object closer to you
    • Negative arguments pushes the object further away from you
  • translate3d() is a shorthand for translateX(), translateZ() and translateZ()
    • For example: translate3d(20px, 0, 3rem) = translateX(20px) translateY(0) translateZ(3rem)

REMARK

# scaleZ() and scale3d()

  • scaleZ(): scales an element up or down along the Z axis
    • Arguments larger than 1 scales the object up (zoom in)
    • Arguments smaller than 1 scales the object down (zoom out)
  • scale3d() is a shorthand for scaleX(), scaleZ() and scaleZ()
    • For example: scale3d(1, 1.5, .7) = scaleX(1) scaleY(1.5) scaleZ(.7)

REMARK

# rotateZ() and rotate3d()

  • rotateZ(): rotates an element around the Z axis
  • rotate3d() is specified by three numbers for the X, Y and Z axes and one angle for the rotation

REMARKS

  • The rotate3d() property is NOT a list of X, Y and Z rotations like translate3d() and scale3d()
  • The three arguments together form a matrix from which ONE axis is calculated (the details of this calculation are beyond the scope of this page) around which the fourth argument (the angle) will rotate

# perspective() method

  • Perspective is an important feature to get a natural sense of depth, especially when rotating over one of the axes
  • The perspective defines how far your eyes are away from the object
    • A larger number means that you are further away from the object and the perspective looks smaller
    • A smaller number means that you are closer to the object and the perspective looks larger
  • There are two ways to generate some depth:
    • The perspective() method as a value of transform gives depth to that specific element
    • The perspective property gives depth on a total scene (that is, on a group of child elements)
  • Remember that when we use rotateX() and rotateY() to rotate along the X and/or Y axis, it looks like the elements are squeezed because, by default, they have no depth
  • By adding the perspective() method in front of rotateX() and rotateY(), you enter the 3D space for this specific element and the rotation looks more natural
  • In the example below:
    • The left animation has no perspective() method





     


     



    #without div span {
      animation: withoutPerspective 4s linear infinite;
    }
    @keyframes withoutPerspective {
      to {
          transform: rotateX(0);
      }
      to {
          transform: rotateX(360deg);
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    • The right animation has the perspective(200px) method IN FRONT OF the rotation method





     


     



    #with div span {
      animation: withoutPerspective 4s linear infinite;
    }
    @keyframes withPerspective {
      to {
          transform: perspective(200px) rotateX(0);
      }
      to {
          transform: perspective(200px) rotateX(360deg);
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
<section id="without">
    <div>
        <span>without perspective</span>
    </div>
</section>
<section id="with">
    <div>
        <span>with perspective</span>
    </div>
</section>

    
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; margin: 2rem; display: flex; flex-wrap: wrap; justify-content: center; }
section { display: flex; flex-wrap: wrap; }
div { border: 1px solid darkgray; background-color: rgba(169, 169, 169, .2); display: inline-block; margin: 1rem; }
span { display: inline-block; width: 150px; line-height: 150px; font-size: .8rem; text-align: center; border: 1px solid #000; background-color: rgba(248, 104, 52, 0.6); }
#without div span { animation: withoutPerspective 4s linear infinite; }
@keyframes withoutPerspective { from { transform: rotateX(0); } to { transform: rotateX(360deg); } }
#with div span { animation: withPerspective 4s linear infinite; /* transform-origin: right; */ }
@keyframes withPerspective { from { transform: perspective(200px) rotateX(0); } to { transform: perspective(200px) rotateX(360deg); } }
    

# Exercises

  1. Play with the different perspective values, e.g. perspective(500px), perspective(100px)
  2. Change the rotation axis from rotateX() to rotateY()
  3. Change thetransform-origin to another rotation point, for example:


 


#with div span {
    animation: withPerspective 4s linear infinite;
    transform-origin: right;
}
1
2
3
4

Note that, by specifying only one value for transform-origin, the other value defaults to center

# perspective property

  • Sometimes you have one object that contains two or more child elements (let's call it a "scene")
  • When using the perspective() method on every child element inside the "scene", this doesn't look very natural at all ...


 
 






 


 



span {
    ...
    animation: rotate 4s linear infinite;
    transform-origin: center;
}
div:hover span {
    animation-play-state: paused;
}
@keyframes rotate {
    from {
        transform: perspective(500px) rotateY(0); 
    }
    to {
        transform: perspective(500px) rotateY(360deg);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    
<div>
    <span>1</span>
    <span>2</span>
    <span>3</span>
    <span>4</span>
    <span>5</span>
    <span>6</span>
    <span>7</span>
    <span>8</span>
    <span>9</span>
</div>

    
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; text-align: center; height: 100vh; display: flex; align-content: center; justify-items: center; }
div { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; width: 80vmin; height: 80vmin; padding: 0 1%; margin: auto; border: 1px solid darkgray; background-color: rgba(169, 169, 169, .2); }
span { height: 30%; flex: 0 0 30%; font-size: 2rem; border: 1px solid #000; background-color: rgba(248, 104, 52, 0.6); animation: rotate 4s linear infinite; transform-origin: center; }
div:hover span { animation-play-state: paused; }
@keyframes rotate { from { transform: perspective(500px) rotateY(0); } to { transform: perspective(500px) rotateY(360deg); } }
    
  • That's where the perspective property comes in action: when we delete the perspective() method from the child elements and replace it with the perspective property on the parent (the div tag) we get a more beautiful 3D perspective for the entire scene
  • Now the rotation on the whole scene looks very natural


 











 


 



div {
    ...
    perspective: 500px;               /* add perspective property for the whole scene */
}
span {
    ...
    animation: rotate 4s linear infinite;
    transform-origin: center;
}
div:hover span {
    animation-play-state: paused;
}
@keyframes rotate {
    from {
        transform: rotateY(0);        /* perspective() methode deleted */
    }
    to {
        transform: rotateY(360deg);   /* perspective() methode deleted */
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    
<div>
    <span>1</span>
    <span>2</span>
    <span>3</span>
    <span>4</span>
    <span>5</span>
    <span>6</span>
    <span>7</span>
    <span>8</span>
    <span>9</span>
</div>

    
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; text-align: center; height: 100vh; display: flex; align-content: center; justify-items: center; }
div { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; width: 80vmin; height: 80vmin; padding: 0 1%; margin: auto; border: 1px solid darkgray; background-color: rgba(169, 169, 169, .2); perspective: 500px; }
span { height: 30%; flex: 0 0 30%; font-size: 2rem; border: 1px solid #000; background-color: rgba(248, 104, 52, 0.6); animation: rotate 4s linear infinite; transform-origin: center; }
div:hover span { animation-play-state: paused; }
@keyframes rotate { from { transform: rotateY(0); } to { transform: rotateY(360deg); } }
    

# perspective() vs perspective

# When to use the perspective() method?

  • If only one element has to be transformed, use the perspective() method within the transform property of the element

# When to use the perspective property?

  • If two or more elements have to be transformed, use the perspective property
  • First create a scene container around those elements, e.g. div.scene, and set the perspective property on this scene
  • Attention: the scene itself MAY NOT have any transformation properties!

# perspective-origin

  • As you already know: the transform-origin property sets the reference point for the transform of a child element
  • The perspective-origin does about the same, but this time for the transform-origin property on the whole scene
  • So, perspective-origin determines the position from which you are looking at the whole scene
  • The values for perspective-origin are exactly the same as for transform-origin
    • The default value is also center center
  • In the example below:
    • All child elements are rotated 90deg along the X axis
    • The whole scene has a perspective of 300px
    • The animation changes the perspective-origin
    • The green dot is on the perspective-origin and represents the view point from where you are looking at the perspective
    
<div>
    <span>1</span>
    <span>2</span>
    <span>3</span>
    <span>4</span>
    <span>5</span>
    <span>6</span>
    <span>7</span>
    <span>8</span>
    <span>9</span>
</div>

    
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; text-align: center; height: 100vh; display: flex; align-content: center; justify-items: center; }
div { background-image: url(https://itf-web-essentials.netlify.app/assets/dot.svg); background-repeat: no-repeat; background-size: 1rem 1rem; background-position: 0% 0%; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; width: 80vmin; height: 80vmin; padding: 0 1%; margin: auto; border: 1px solid darkgray; background-color: rgba(169, 169, 169, .2); perspective: 300px; animation: moveOrigin infinite 10s; }
span { height: 30%; flex: 0 0 30%; font-size: 2rem; border: 1px solid #000; background-color: rgba(248, 104, 52, 0.6); transform: rotateX(90deg); /* transform-origin: left; */ }
@keyframes moveOrigin { 0% { perspective-origin: 0% 0%; background-position: 0% 0%; } 25% { perspective-origin: 100% 0%; background-position: 100% 0%; } 33% { perspective-origin: 100% 33%; background-position: 100% 33%; } 50% { perspective-origin: 50% 50%; background-position: 50% 50%; } 66% { perspective-origin: 66% 100%; background-position: 66% 100%; } 75% { perspective-origin: 10% 90%; background-position: 10% 90%; } 100% { perspective-origin: 0% 0%; background-position: 0% 0%; } }
    

# Exercises

  1. Enable the transform-origin property and with some the different settings, e.g. top, bottom, ...
  2. Modify the value of rotateX() and look what happens
  3. Rename the property rotateX() to rotateY() and look what happens

# transform-style

  • The transform-style property determines whether the DIRECT child elements are flattened in its 2D plane or shown in a 3D space
  • By default, all direct children are rendered in a flat, 2D plane (transform-style on parent is default flat)
  • To allow all direct children to be transformed on the Z axis (translateZ(), rotateZ() and/or scaleZ()) , you have to EXPLICITLY set the property of the PARENT element to transform-style: preserve-3d
  • In the example below:
    • Because we are going to transform several elements, we are going to use a scene with a global perspective


     


    div.scene {
        ...
        perspective: 2000px;  /* perspective for the whole scene */
    }
    
    1
    2
    3
    4
    • div.parent is the first element inside the scene and rotates along its Y axis


     


     
     


    div.parent {
        ...
        animation: rotate 5s linear infinite;
    }
    @keyframes rotate {
        from { transform: rotateY(0); }
        to { transform: rotateY(360deg); }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    • Inside div.parent are two child elements which have a different translateZ() value
      • The red box has to move 5rem closer to the viewer transform: translateZ(5rem);
      • The green box has to move 8rem closer to the viewer transform: translateZ(8rem);


     



     


    span:first-child {
        ...
        transform: translateZ(5rem);
    }
    span:last-child {
        ...
        transform: translateZ(8rem);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    • As you can see, the two child elements are flattened in their parent element (div.parent) and their translateZ() methods don't work!
    
<div class="scene">
    <div class="parent">
        div.parent
        <span>translateZ(5rem)</span>
        <span>translateZ(8rem)</span>
    </div>
</div>

    
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; margin: 3rem; }
div.scene { outline: 1px solid whitesmoke; width: 300px; height: 300px; margin: auto; display: flex; justify-content: center; align-items: center; perspective: 2000px; /* perspective for the whole scene */ }
div.parent { width: 90%; height: 90%; position: relative; border: 1px solid darkgray; background-color: rgba(101, 176, 251, 0.2); animation: rotate 5s linear infinite; /* transform-style: preserve-3d; */ }
@keyframes rotate { from { transform: rotateY(0); } to { transform: rotateY(360deg); } }
span { position: absolute; text-align: center; border: 1px solid darkgray; font-size: .8rem; }
span:first-child { top: 20px; left: 20px; width: 200px; line-height: 200px; background-color: rgba(251, 101, 101, 0.2); transform: translateZ(5rem); }
span:last-child { top: 50px; left: 50px; width: 250px; line-height: 100px; background-color: rgba(52, 248, 117, 0.6); transform: translateZ(8rem); }
    
  • All you have to do to fix this behavior is enabling the 3D space on the parent element



 


div.parent {
    ...
    animation: rotate 5s linear infinite;
    transform-style: preserve-3d;
}
1
2
3
4
5

# Examples

# 3D figcaption

  • Let's start from a finished transition example, in which a figcaption is rotated around the X axis (from 90deg = invisible to 0 = visible) when hovering its enclosing figure
  • All you have to do to change the squeezed figcaption to a nice, foldable version, is adding some perspective to the transformation
    • Add perspective(300px) as the first method to the transformation and change the start rotation to 60deg


 



figcaption {
   ...
   transform: perspective(300px) rotateX(60deg);
   ...
}
1
2
3
4
5
    
<figure>
    <img src="https://picsum.photos/id/1080/300/300" alt="Strawberries">
    <figcaption>Strawberries</figcaption>
</figure>

    
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; padding: 1rem; text-align: center; }
figure { border: 1px solid darkgray; line-height: 0; display: inline-block; position: relative; }
figcaption { position: absolute; bottom: 0; left: 0; width: 100%; font-size: 2rem; line-height: 1.5; background-color: rgba(255, 255, 255, 0.3); color: white; transform: rotateX(90deg); /* change to: perspective(300px) rotateX(60deg) */ transform-origin: bottom center; transition: transform .5s; }
figure:hover figcaption { transform: rotateX(0); }
    

# Flip card

  • The flip card (div.scene) contains two children (div.front and div.back)
  • By default, the front and the back of the card stand beneath each other
    
<div class="scene">
    <div class="front">
        <h1>Front</h1>
    </div>
    <div class="back">
        <h1>Back</h1>
    </div>
</div>

    
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; padding: 1rem; text-align: center; height: 100vh; display: flex; justify-content: center; align-items: center; }
h1 { margin-top: 3rem; color: firebrick; /* transform: translateZ(2rem); */ }
.scene { /* outline: 1px solid lightgray; */ /* width: 200px; height: 300px; position: relative; */ /* perspective: 500px; */ /* transform-style: preserve-3d; */ }
.front, .back { width: 200px; height: 300px; border: 1px solid #000; box-shadow: 1rem 1rem 2rem rgba(0, 0, 0, .3); /* transition: 2s; */ /* backface-visibility: hidden; */ /* position: absolute; top: 0; left: 0; */ /* transform-style: preserve-3d; */ }
.front { background-image: url(https://picsum.photos/id/1028/200/300); }
.back { background-image: url(https://picsum.photos/id/1028/200/300?grayscale); /* transform: rotateY(180deg); */ }
.scene:hover .front { /* transform: rotateY(180deg); */ }
.scene:hover .back { /* transform: rotateY(360deg); */ }
    
  • Step 1: Rotate the back of the card
    • At the end only the front is visible in the default state and the back is visible on the hover state
    • We have to rotate the back by 180deg on the Y axis, to avoid that the text on the reverse side is mirrored in the hover state


       


      .back {
          background-color: rgba(128, 201, 240, 0.6);
          transform: rotateY(180deg);
      }
      
      1
      2
      3
      4
  • Step 2: Flip the card
    • To flip the whole card, we rotate the front and the back 180deg on the hover state of the card (div.scene)
    • Because the back already starts from 180deg onward, it continues to rotate to 360deg


       


       


       


      .front, .back {
          ...
          transition: 2s;
      }
      .scene:hover .front {
          transform: rotateY(180deg);
      }
      .scene:hover .back {
          transform: rotateY(360deg);
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
  • Step 3: Disable the backface
    • Only the part of the card that is not mirrored may remain visible
    • If we do not do this, we will always see the back when we put the front and the back on top of each other



       


      .front, .back {
          ...
          transition: 2s;
          backface-visibility: hidden;
      }
      
      1
      2
      3
      4
      5
  • Step 4: Position the front and the back on top of each other
    • Position the front and the back absolutely inside the card container

       
       
       



       
       
       


      .scene {
         width: 200px;
         height: 300px;
         position: relative;
      }
      .front, .back {
         ...
         position: absolute;
         top: 0;
         left: 0; 
      } 
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
  • Step 5: Add perspective to the scene
    • All you have to do to change the squeezed rotation to a nice 3D version, is adding some perspective to the whole scene and not to the individual elements




       


      .scene {
          width: 200px;
          height: 300px;
          position: relative;
          perspective: 500px; 
      }
      
      1
      2
      3
      4
      5
      6
  • Final step: Put the text in front of the card
    • Translate the text by 2rem on the Z axis to place it in front of the rest of the card
    • By default, the Z axis is rendered inside a flat surface, that's why we have to explicitly enable 3D on all its parent elements
    • The parent element for the text 'front' is div.front and the parent for the text 'back' is div.back
      The parent element for div.front and div.back is div.scene



       




       



       


      h1 {
          margin-top: 3rem;
          color: firebrick;
          transform: translateZ(2rem);
      }
      .scene {
          ...
          perspective: 500px; 
          transform-style: preserve-3d;
      }
      .front, .back {
          ...
          transform-style: preserve-3d;
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14

# Flapping image

  • Our scene contains two levels of nested elements, and they both contain their own animation
    • The figure rotates around the X axis
    • The img, inside the figure, "flaps" on the top of the figure
    
<div class="scene">
    <figure>
        <img src="https://picsum.photos/id/855/300/200" alt="">
        <figcaption>Photo @ Rodion Kutsaev</figcaption>
    </figure>
</div>

    
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; height: 100vh; display: flex; justify-content: center; align-items: center; font-weight: bold; }
.scene { /* perspective: 1000px;*/ }
figure { text-align: center; width: 300px; margin: auto; border: 1px solid #000; border-top-width: 1rem; /* transform-style: preserve-3d; */ }
.scene figure { /* animation: rotateFigure 10s linear infinite; */ }
.scene:hover figure { /* animation-play-state: paused; */ }
@keyframes rotateFigure { from { transform: rotateY(0); } to { transform: rotateY(360deg); } }
img { vertical-align: middle; width: 100%; height: auto; /* animation: flapImage 700ms ease-in-out infinite alternate; transform-origin: top; */ }
@keyframes flapImage { from { transform: rotateX(-40deg); } to { transform: rotateX(40deg); } }
    
  • Step 1: Add perspective to the scene

     


    .scene {
        perspective: 1000px;
    }
    
    1
    2
    3
  • Step 2: Let the image "flap"
    • Add an alternating animation to the img tag (rotate between -40deg and 40deg)
    • Set the transform-origin to the top of the image




     
     






    img {
        vertical-align: middle;
        width: 100%;
        height: auto;
        animation: flapImage 700ms ease-in-out infinite alternate;
        transform-origin: top;
    }
    @keyframes flapImage {
        from { transform: rotateX(-40deg); }
        to { transform: rotateX(40deg); }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • Step 3: Rotate the figure
    • Rotate the figure around his Y axis
    • Pause the animation on the hover state of the scene

     


     






    .scene figure {
        animation: rotateFigure 10s linear infinite;
    }
    .scene:hover figure {
        animation-play-state: paused;
    }
    @keyframes rotateFigure {
        from { transform: rotateY(0); }    
        to { transform: rotateY(360deg); }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • Final step: Preserve 3D on the figure
    • The image stops to flap because the figure has his own transform now!
      (Remember that child elements are rendered on a flat pane by default)
    • Enable 3D on the figure


     


    figure {
        ...
        transform-style: preserve-3d;
    }
    
    1
    2
    3
    4
Last Updated: 12/11/2020, 7:17:56 PM