<a class="link" target="_blank" rel="noopener" href="https://vinceumo.github.io/devNotes/CSS/css-3d-scrolling-on-the-z-axis/">Read article</a>
<h1>
  <img
       class="logo"
       src="https://vinceumo.github.io/devNotes/images/CSS3d-scrolling-z-axis-example-logo.svg"
       alt="Studio Ghibli Logo"
       />
  Studio Ghibli's Movies
</h1>
<div class="viewport">
  <div class="scene3D-container">
    <div class="scene3D"></div>
  </div>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
:root {
  --scenePerspective: 1;
  --scenePerspectiveOriginX: 50;
  --scenePerspectiveOriginY: 30;
  --itemZ: 2;
  --cameraSpeed: 150; // Where 1 is the fastest
  --cameraZ: 0;
  --viewportHeight: 0;
}

// Set 3D scene

.viewport {
  height: calc(var(--viewportHeight) * 1px);

  .scene3D-container {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    perspective: calc(var(--scenePerspective) * var(--cameraSpeed) * 1px);
    perspective-origin: calc(var(--scenePerspectiveOriginX) * 1%)
      calc(var(--scenePerspectiveOriginY) * 1%);
    will-change: perspective-origin;
    transform: translate3d(0, 0, 0); //Allow Hardware-Accelerated CSS

    .scene3D {
      position: absolute;
      top: 0;
      height: 100vh;
      width: 100%;
      transform-style: preserve-3d;
      transform: translateZ(calc(var(--cameraZ) * 1px));
      will-change: transform;

      > div {
        position: absolute;
        display: block;
        width: 100%;
        top: 40%;

        @media only screen and (min-width: 600px) {
          width: 45%;
        }

        &:nth-child(2n) {
          left: 0;
        }

        &:nth-child(2n + 1) {
          right: 0;
        }

        @for $i from 0 through 25 {
          &:nth-child(#{$i}) {
            transform: translate3D(
              random(50) - 25 * 1%,
              random(100) - 50 * 1%,
              calc(var(--itemZ) * var(--cameraSpeed) * #{$i} * -1px)
            );
          }
        }
      }
    }
  }
}



// ----------
// Styling
// ----------
@import url("https://fonts.googleapis.com/css?family=Playfair+Display+SC:900");

*,
*:before,
*:after {
  box-sizing: border-box;
}

body {
  background-color: hsl(231, 15%, 18%);
  color: hsl(231, 15%, 18%);
  padding: 0;
  margin: 0;
}

h1 {
  font-family: "Playfair Display SC", serif;
  font-size: 1.5rem;
  color: hsl(0, 0%, 100%);
  margin: 0;
  position: fixed;
  bottom: 1rem;
  left: 1rem;
  text-align: center;
  .logo {
    display: block;
    margin: 0 auto;
    max-width: 150px;
  }
}

.link {
  color: hsl(0, 0%, 100%);
  margin: 0;
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 1;
}

.scene3D {
  > div {
    padding: 2rem;

    h2 {
      margin-top: 0;
      font-family: "Playfair Display SC", serif;
      font-size: 1.5rem;
    }

    @for $i from 0 through 25 {
      &:nth-child(#{$i}) {
        background-color: hsl(-30 + ($i * 30), 100%, 88%);
      }
    }
  }
}
View Compiled
let films = [];

const perspectiveOrigin = {
  x: parseFloat(
    getComputedStyle(document.documentElement).getPropertyValue(
      "--scenePerspectiveOriginX"
    )
  ),
  y: parseFloat(
    getComputedStyle(document.documentElement).getPropertyValue(
      "--scenePerspectiveOriginY"
    )
  ),
  maxGap: 10
};

document.addEventListener("DOMContentLoaded", function() {
  axios
    .get("https://ghibliapi.herokuapp.com/films")
    .then(function(response) {
      films = response.data;
      appendFilms(films);
      window.addEventListener("scroll", moveCamera);
      window.addEventListener("mousemove", moveCameraAngle);
      setSceneHeight();
    })
    .catch(function(error) {
      console.log(error);
    });
});

function moveCameraAngle(event) {
  const xGap =
    (((event.clientX - window.innerWidth / 2) * 100) /
      (window.innerWidth / 2)) *
    -1;
  const yGap =
    (((event.clientY - window.innerHeight / 2) * 100) /
      (window.innerHeight / 2)) *
    -1;
  const newPerspectiveOriginX =
    perspectiveOrigin.x + (xGap * perspectiveOrigin.maxGap) / 100;
  const newPerspectiveOriginY =
    perspectiveOrigin.y + (yGap * perspectiveOrigin.maxGap) / 100;

  document.documentElement.style.setProperty(
    "--scenePerspectiveOriginX",
    newPerspectiveOriginX
  );
  document.documentElement.style.setProperty(
    "--scenePerspectiveOriginY",
    newPerspectiveOriginY
  );
}

function moveCamera() {
  document.documentElement.style.setProperty("--cameraZ", window.pageYOffset);
}

function setSceneHeight() {
  const numberOfItems = films.length; // Or number of items you have in `.scene3D`
  const itemZ = parseFloat(
    getComputedStyle(document.documentElement).getPropertyValue("--itemZ")
  );
  const scenePerspective = parseFloat(
    getComputedStyle(document.documentElement).getPropertyValue(
      "--scenePerspective"
    )
  );
  const cameraSpeed = parseFloat(
    getComputedStyle(document.documentElement).getPropertyValue("--cameraSpeed")
  );

  const height =
    window.innerHeight +
    scenePerspective * cameraSpeed +
    itemZ * cameraSpeed * numberOfItems;

  // Update --viewportHeight value
  document.documentElement.style.setProperty("--viewportHeight", height);
}

function createFilmItem(film) {
  return `<div>
    <h2>${film.title}</h2>
    <p>Year: ${film.release_date}</p>
    <p>Director: ${film.director}</p>
    <p>${film.description}</p>
  </div>`;
}

function appendFilms(films) {
  const filmsEl = document.querySelector(".viewport .scene3D");
  let filmsNodes = [];

  for (film of films) {
    filmsNodes.push(createFilmItem(film));
  }

  filmsEl.innerHTML = filmsNodes.join(" ");
}
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.