javascripthtmlgsap

3D animation of a book opening - unexpected rotation


I know I'm missing something with the rotation of the inside cover when the book is "clicked on" to open, but I am not seeing it.

The expectation is that when the book is clicked on, the following happens:

  1. The cover of the book mirrors across the Y-axis
  2. The first page is shown on the right with an border that resembles the background of the back cover.
  3. The inside cover is shown as a solid cover that matches the background color of the front cover.
  4. The pages then flip across the Y-axis

All in all I am trying to get a 3D Animation of a book opening when it is clicked on by the user.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Opening Book with GSAP</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
  <style>
    body {
      font-family: 'Inter', sans-serif;
      background-color: #f0f4f8;
      overflow: hidden;
    }

    .font-serif {
      font-family: 'Playfair Display', serif;
    }

    /* The scene is the 3D space for the book */
    .scene {
      display: flex;
      align-items: center;
      justify-content: center;
      height: 100vh;
      perspective: 2500px;
    }

    /* The wrapper handles positioning and can be clicked */
    .book-wrapper {
      position: relative;
      cursor: pointer;
    }

    /* The book container holds all the 3D pieces */
    .book {
      width: 350px;
      height: 500px;
      position: relative;
      transform-style: preserve-3d;
    }

    /* The front cover, which will flip open */
    .front-cover {
      position: absolute;
      width: 100%;
      height: 100%;
      top: 0;
      left: 0;
      transform-origin: left center;
      transform-style: preserve-3d;
      z-index: 10;
    }

    .cover-face {
      position: absolute;
      width: 100%;
      height: 100%;
      backface-visibility: hidden;
      border-radius: 0.5rem;
      background-color: #4a3a32;
    }

    .cover-face-front {
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 2rem;
      position: relative;
      overflow: hidden;
    }

    /* The inside of the cover matches the outside */
    .cover-face-back {
      background-color: #4a3a32;
      transform: rotateY(-180deg);
    }

    /* Crease styles */
    .cover-face-front::before,
    .cover-face-front::after {
      content: '';
      position: absolute;
      top: 0;
      height: 100%;
      width: 2px;
      background-color: rgba(0, 0, 0, 0.2);
      box-shadow: 1px 0 5px rgba(0, 0, 0, 0.35);
    }

    .cover-face-front::before {
      left: 31px;
    }

    .cover-face-front::after {
      left: 35px;
    }

    /* NEW: This is the actual back cover board */
    .back-cover {
      position: absolute;
      width: 100%;
      height: 100%;
      top: 0;
      left: 0;
      background-color: #4a3a32;
      border-radius: 0.5rem;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
      z-index: 1;
    }

    /* The static page block that sits on top of the back cover */
    .pages {
      position: absolute;
      width: calc(100% - 1rem);
      height: calc(100% - 1rem);
      top: 0.5rem;
      left: 0.5rem;
      background-color: #f3f0e9;
      border-radius: 0.25rem;
      z-index: 5;
    }

    /* Styles for the flipping pages */
    .flipping-page {
      position: absolute;
      width: calc(100% - 1rem);
      height: calc(100% - 1rem);
      top: 0.5rem;
      left: 0.5rem;
      background-color: #fdfaf2;
      transform-origin: left center;
      border-radius: 0.25rem;
      box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1) inset;
      border-right: 1px solid #e0d9cd;
      z-index: 6;
    }

    /* Decorative elements */
    .cover-design {
      border: 4px double #d4af37;
      width: 100%;
      height: 100%;
      border-radius: 0.25rem;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
      padding: 1rem;
      color: #d4af37;
    }

    .cover-title {
      font-family: 'Playfair Display', serif;
      font-size: 2.75rem;
      font-weight: 700;
      letter-spacing: 1px;
      text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4);
    }

    .cover-author {
      margin-top: 1.5rem;
      font-size: 1.125rem;
      font-style: italic;
      border-top: 1px solid rgba(212, 175, 55, 0.5);
      padding-top: 1.5rem;
    }
  </style>
</head>

<body>

  <div class="scene">
    <!-- The new wrapper handles positioning -->
    <div id="book-wrapper" class="book-wrapper">
      <!-- The book itself only handles 3D animations -->
      <div id="book" class="book">
        <!-- NEW: Added a dedicated back cover element -->
        <div class="back-cover"></div>
        <div class="pages"></div>
        <div class="flipping-page" id="page-3"></div>
        <div class="flipping-page" id="page-2"></div>
        <div class="flipping-page" id="page-1"></div>
        <div class="front-cover">
          <div class="cover-face cover-face-front">
            <div class="cover-design">
              <h1 class="cover-title">About the Author</h1>
              <p class="cover-author"></p>
            </div>
          </div>
          <div class="cover-face cover-face-back"></div>
        </div>
      </div>
    </div>
  </div>

  <script>
    // Select the elements to animate
    const bookWrapper = document.getElementById('book-wrapper');
    const frontCover = document.querySelector('.front-cover');
    const flippingPages = gsap.utils.toArray('.flipping-page');

    // Set the default transform origin for the cover and pages
    gsap.set([frontCover, flippingPages], {
      transformOrigin: "left center"
    });

    // Create a GSAP timeline, paused by default
    const timeline = gsap.timeline({
      paused: true
    });

    // Add animations to the timeline
    timeline
      // 1. Move the entire book wrapper to the right to center the spine
      .to(bookWrapper, {
        x: 175,
        duration: 1.2,
        ease: "power2.inOut"
      })
      // 2. Flip the cover open at the same time
      .to(frontCover, {
        rotationY: -180,
        duration: 1.2,
        ease: "power2.inOut"
      }, "<") // "<" starts at the same time as the previous animation
      // 3. Flip the pages with a stagger effect, starting slightly after the cover begins to open
      .to(flippingPages, {
        rotationY: -180,
        duration: 0.8,
        ease: "power1.inOut",
        stagger: 0.1
      }, "<0.2"); // "<0.2" starts 0.2s after the previous animation's start

    // Event listener to control the timeline
    bookWrapper.addEventListener('click', () => {
      if (timeline.reversed() || timeline.progress() === 0) {
        timeline.play();
      } else {
        timeline.reverse();
      }
    });
  </script>

</body>

</html>

Images: Before and after, respectively. Closed book

Open book


Solution

  • I was able to reproduce and fix the issue by making only two changes to your file. The issue is that when the book opens, the back side of the front cover shows below the book instead of being under the flipped page. I resolved it by adding position, width, height, top, and left to existing class cover-face-back:

            .cover-face-back {
                background-color: #4a3a32; 
                transform: rotateY(-180deg);
                position: absolute;
                width: 100%;
                height: 100%;
                top: 0;
                left: 0;
             }
    

    Now the cover-face-back element is in the correct position, but it covers the flipped page. To solve this, I modified the timeline applied to the flippingPages: added zIndex

     .to(flippingPages, {
         zIndex: 11, // display flipped page on top of the front cover
         rotationY: -180,
         duration: 0.8,
         ease: "power1.inOut",
         stagger: 0.1
     }, "<0.2"); // "<0.2" starts 0.2s after the previous animation's start
    

    Now the book opens as you would expect (you can adjust the position if you don't like that the left border looks a little wider than the right, but to me it looks natural): enter image description here