<div id="container">
  <div id="square" class="square">
  <h4 class="v v1">1</h4>
  <h4 class="v v2">2</h4>
  <h4 class="v v3">3</h4>
  <h4 class="v v4">4</h4>
</div>
  <div id="square2" class="square">
  <h4 class="v v1">1</h4>
  <h4 class="v v2">2</h4>
  <h4 class="v v3">3</h4>
  <h4 class="v v4">4</h4>
</div>
</div>

<div id="scrollElement"></div>
body {
  width: 100vw;
  display: flex;
  justify-content: center;
  align-items: center;
}

#container {
  position:fixed;
  top:0;
  left:0;
  width: 100%;
  height:100%;
  transform-style: preserve-3d;
  perspective: 1000px;
  display: flex;
  justify-content: center;
  align-items: center;
}

#square, #square2 {
  width: 200px;
  height: 200px;
  background: red;
  position:relative;
  margin:120px;
  transform-style: preserve-3d;
}

.square h4 {
  margin:0;
  color:#000;
  font-size:30px;
  font-weight: bold;
  background: #FFF;
  padding:3px;
  width:40px;
  height:40px;
  border-radius: 5px;
  border: 2px solid #000;
  text-align: center;
}

.v {
  position:absolute;
}

.v1 {
  top:10px;
  left:10px;
}

.v2 {
  top:10px;
  right:10px;
}

.v3 {
  bottom:10px;
  right:10px;
}

.v4 {
  bottom:10px;
  left:10px;
}

#scrollElement {
  height: 10000px;
  width: 100vw;
}
gsap.registerPlugin(ScrollTrigger);

const tl = gsap.timeline();

ScrollTrigger.create({
  animation: tl,
  trigger: "#scrollElement",
  start: "top top",
  end: "100% 100%",
  scrub: 0.2
});

// Square on the left:
// 1 - tilt on X  Axis
// 2 - tilt on Y Axis
// Result: Y axis won't coincide with the square Y axis, ( yellow )

tl.to("#square", { rotationX: 60, ease: "circ.out", duration: 5 })
  .to("#square", { rotationY: 90, ease: "circ.out", duration: 5 })
  .to("#square2", { rotationY: 60, ease: "circ.out", duration: 5 }, 0)
  .to("#square2", { rotationX: 90, ease: "circ.out", duration: 5 }, ">");

// Square on the right:
// 1 - tilt on Y  Axis
// 2 - tilt on X Axis
// Result: X axis now coincides with the square Y axis, ( green ) but it keeps being the document Y axis

////////////////////////////////////////

const elems = document.querySelectorAll(".square");
elems.forEach((el) => {
  const { width, height } = el.getBoundingClientRect();
  console.log(width, height);
  const axes = {
    x: {
      rotate: "rotateZ(90deg)",
      rotatePerp: "rotateZ(90deg) rotateY(90deg)",
      top: "50%",
      left: "50%",
      translate: `translate(0, -${height}px)`,
      translatePerp: `translate3d(0px, -${height}px, -${height}px)`,
      color: "green"
    },
    y: {
      rotate: "",
      rotatePerp: "rotateY(90deg)",
      top: "50%",
      left: "50%",
      translate: `translate(0, -${height}px)`,
      color: "yellow"
    },
    z: {
      rotate: "rotateZ(90deg) rotateX(90deg)",
      rotatePerp: "rotateX(90deg)",
      top: "50%",
      left: "50%",
      translate: `translate(0px, -${height}px)`,
      color: "blue"
    }
  };
  for (const key in axes) {
    const axis = axes[key];
    const div = document.createElement("div");
    const div1 = document.createElement("div");
    div.id = key;
    div.style.transform = axis.translate + " " + axis.rotate;
    div.style.top = axis.top;
    div.style.left = axis.left;
    div.style.backgroundColor = axis.color;
    div.style.width = "5px";
    div.style.height = height * 2 + "px";
    div.style.position = "absolute";
    div.innerHTML = `<h4>${key}</h4>`;
    // Perpendicular axis
    div1.id = key + "_" + "perp";
    div1.style.transform = axis.translate + " " + axis.rotatePerp;
    div1.style.top = axis.top;
    div1.style.left = axis.left;
    div1.style.backgroundColor = axis.color;
    div1.style.width = "5px";
    div1.style.height = height * 2 + "px";
    div1.style.position = "absolute";
    div1.innerHTML = `<h4>${key}</h4>`;
    el.appendChild(div);
    el.appendChild(div1);
  }
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.co/gsap@3/dist/gsap.min.js
  2. https://unpkg.com/gsap@3/dist/ScrollTrigger.min.js