<div class="content">
  <div class="loading">Loading</div>
  <div class="trigger"></div>
  <div class="section">
    
    <h1>Airplanes.</h1>
    <h3>The beginners guide.</h3>
    <p>You've probably forgotten what these are.</p>
<!--    <div class="phonetic">/ ˈɛərˌpleɪn /</div> -->
    <div class="scroll-cta">Scroll</div>
  </div>
  
  <div class="section right">
    <h2>They're kinda like buses...</h2>
  </div>
  
  <div class="ground-container">
    <div class="parallax ground"></div>
    <div class="section right">
      <h2>..except they leave the ground.</h2>
      <p>Saaay what!?.</p>
    </div>

    <div class="section">
      <h2>They fly through the sky.</h2>
      <p>For realsies!</p>
    </div>
    
    <div class="section right">
      <h2>Defying all known physical laws.</h2>
      <p>It's actual magic!</p>
    </div>
    <div class="parallax clouds"></div>
  </div>
  
  <div class="blueprint">
    <svg width="100%" height="100%" viewbox="0 0 100 100">
      <line id="line-length" x1="10" y1="80" x2="90" y2="80" stroke-width="0.5"></line>
      <path id="line-wingspan" d="M10 50, L40 35, M60 35 L90 50" stroke-width="0.5"></path>
      <circle id="circle-phalange" cx="60" cy="60" r="15" fill="transparent" stroke-width="0.5"></circle>
    </svg>
    <div class="section dark ">
      <h2>The facts and figures.</h2>
      <p>Lets get into the nitty gritty...</p>
    </div>
    <div class="section dark length">
      <h2>Length.</h2>
      <p>Long.</p>
    </div>
    <div class="section dark wingspan">
      <h2>Wing Span.</h2>
      <p>I dunno, longer than a cat probably.</p>
    </div>
    <div class="section dark phalange">
      <h2>Left Phalange</h2>
      <p>Missing</p>
    </div>
    <div class="section dark">
      <h2>Engines</h2>
      <p>Turbine funtime</p>
    </div>
<!--    <div class="section"></div> -->
  </div>
  <div class="sunset">
    <div class="section"></div>
    <div class="section end">
      <h2>Fin.</h2>
      <ul class="credits">
        <li>Plane model by <a href="https://poly.google.com/view/8ciDd9k8wha" target="_blank">Google</a></li>
        <li>Animated using <a href="https://greensock.com/scrolltrigger/" target="_blank">GSAP ScrollTrigger</a></li>
      </ul>
    </div>
  </div>
</div>
@import url('https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&display=swap');
svg
{
  z-index: 100;
}
:root
{
  --padding: 10vmin;
  --color-background: #D0CBC7;
  
  --font-size-large: 8vw;
  --font-size-medium: 4vw;
  --font-size-normal: 2vw;
  
  @media only screen and (min-width: 800px) {
    --font-size-large: 64px;
    --font-size-medium: 32px;
    --font-size-normal: 16px;
  }
  
  @media only screen and (max-width: 500px) {
    --font-size-large: 40px;
    --font-size-medium: 20px;
    --font-size-normal: 14px;
  }
}

a
{
  color: white;
}

ul
{
  margin: 0;
  padding: 0;
  list-style: none;
}

li
{
  margin-top: 10px;
}

html, body
{
  margin: 0;
  // overflow: hidden;
  min-height: 100%;
  min-width: 100%;
  font-family: 'Libre Baskerville', serif;
  background-color: var(--color-background);
  font-weight: 400;
  font-size: var(--font-size-normal);
  overflow-x: hidden;
}

canvas
{
  position: fixed;
  z-index: 10;
  top: 0;
  left: 0;
  z-index: 2;
  pointer-events: none;
  
  visibility: hidden;
  opacity: 0;
}


.solid
{
  clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}

.wireframe
{
  clip-path: polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%);
}

.content
{
  position: relative;
  z-index: 1;
  
  
  .trigger
  {
    position: absolute;
    top: 0;
    height: 100%;
  }
  
  .section
  {
    position: relative;
    padding: var(--padding);
    --pad2: calc(var(--padding) * 2);
    width: calc(100vw - var(--pad2));
    height: calc(100vh - var(--pad2));
    // min-height: 400px;
    margin: 0 auto;
    z-index: 2;
    
    &.dark
    {
      color: white;
      background-color: black;
    }
    
    &.right
    {
      text-align: right;
    }
    
    
  }
  
  .blueprint
  {
    position: relative;
    background-color:#131C2A;
    background-image: linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px),
    linear-gradient(rgba(255,255,255,0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px);
    background-size:100px 100px, 100px 100px, 20px 20px, 20px 20px;
    background-position:-2px -2px, -2px -2px, -1px -1px, -1px -1px;
    background-attachment: fixed;
    
    svg
    {
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      stroke: white;
      pointer-events: none;
      visibility: hidden;
    }
    
    .dark
    {
      background-color: transparent;
    }
  }
  
  .ground-container
  {
    position: relative;
    overflow: hidden;
    // perspective: 2px;
    
    .parallax
    {
      position:absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: -100px;
      background-repeat: no-repeat;
      background-position: top center; 
        background-size: cover;
      transform-origin: top center;
    }
    
    .ground
    {
      z-index: -1;
      background-image: url("https://assets.codepen.io/557388/background-reduced.jpg");   
    }
    
    .clouds
    {
      z-index: 2;
      background-image: url("https://assets.codepen.io/557388/clouds.png");
    }
  }
  
  
  .scroll-cta, .credits
  {
    position: absolute;
    bottom: var(--padding);
  } 
  
  .scroll-cta
  {
    font-size: var(--font-size-medium);
    opacity: 0;
  }
  
  .sunset
  {
    background: url("https://assets.codepen.io/557388/sunset-reduced.jpg") no-repeat top center; 
      background-size: cover;
    transform-origin: top center;
  }
  
  h1, h2
  {
    font-size: var(--font-size-large);
    margin: 0vmin 0 2vmin 0 ;
    font-weight: 700;
    display: inline;
  }
  
  h3,
  {
    font-size: var(--font-size-medium);
    font-weight: 400;
    margin: 0;
  }
  
  .end h2
  {
    margin-bottom: 50vh;
  }
  
  .loading
  {
    position: fixed;
    width: 100vw;
    height: 100vh;
    top: 0;
    left: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--font-size-medium);
  }
  
}
View Compiled
// clearing the console (just a CodePen thing)

console.clear();

// there are 3 parts to this
//
// Scene:           Setups and manages threejs rendering
// loadModel:       Loads the 3d obj file
// setupAnimation:  Creates all the GSAP 
//                  animtions and scroll triggers 
//
// first we call loadModel, once complete we call
// setupAnimation which creates a new Scene

class Scene
{
  constructor(model)
  {
    this.views = [
      { bottom: 0, height: 1 },
      { bottom: 0, height: 0 }
    ];
    
    this.renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true
    });
    
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    this.renderer.setPixelRatio(window.devicePixelRatio);

    document.body.appendChild( this.renderer.domElement );
    
    // scene

    this.scene = new THREE.Scene();
    
    for ( var ii = 0; ii < this.views.length; ++ ii ) {

      var view = this.views[ ii ];
      var camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 2000 );
      camera.position.fromArray([0, 0, 180] );
      camera.layers.disableAll();
      camera.layers.enable( ii );
      view.camera = camera;
      camera.lookAt(new THREE.Vector3(0, 5, 0));
    }
    
    //light

    this.light = new THREE.PointLight( 0xffffff, 0.75 );
    this.light.position.z = 150;
    this.light.position.x = 70;
    this.light.position.y = -20;
    this.scene.add( this.light );

    this.softLight = new THREE.AmbientLight( 0xffffff, 1.5 );
    this.scene.add(this.softLight)

    // group

    this.onResize();
    window.addEventListener( 'resize', this.onResize, false );
    
    var edges = new THREE.EdgesGeometry( model.children[ 0 ].geometry );
    let line = new THREE.LineSegments( edges );
    line.material.depthTest = false;
    line.material.opacity = 0.5;
    line.material.transparent = true;
    line.position.x = 0.5;
    line.position.z = -1;
    line.position.y = 0.2;  
    
    this.modelGroup = new THREE.Group();
    
    model.layers.set( 0 );
    line.layers.set( 1 );
      
    this.modelGroup.add(model);
    this.modelGroup.add(line);
    this.scene.add(this.modelGroup);
  }
  
  render = () =>
  {
    for ( var ii = 0; ii < this.views.length; ++ ii ) {

      var view = this.views[ ii ];
      var camera = view.camera;

      var bottom = Math.floor( this.h * view.bottom );
      var height = Math.floor( this.h * view.height );

      this.renderer.setViewport( 0, 0, this.w, this.h );
      this.renderer.setScissor( 0, bottom, this.w, height );
      this.renderer.setScissorTest( true );

      camera.aspect = this.w / this.h;
      this.renderer.render( this.scene, camera );
    }
  }
  
  onResize = () => 
  {
    this.w = window.innerWidth;
    this.h = window.innerHeight;
    
    for ( var ii = 0; ii < this.views.length; ++ ii ) {
      var view = this.views[ ii ];
      var camera = view.camera;
      camera.aspect = this.w / this.h;
      let camZ = (screen.width - (this.w * 1)) / 3;
      camera.position.z = camZ < 180 ? 180 : camZ;
      camera.updateProjectionMatrix();
    }

    this.renderer.setSize( this.w, this.h );    
    this.render();
  }
}

function loadModel() 
{
  gsap.registerPlugin(ScrollTrigger);
  gsap.registerPlugin(DrawSVGPlugin);
  gsap.set('#line-length', {drawSVG: 0})
  gsap.set('#line-wingspan', {drawSVG: 0})
  gsap.set('#circle-phalange', {drawSVG: 0})
  
  var object;

  function onModelLoaded() {
    object.traverse( function ( child ) {
      let mat = new THREE.MeshPhongMaterial( { color: 0x171511, specular: 0xD0CBC7, shininess: 5, flatShading: true } );
      child.material = mat;
    });

    setupAnimation(object);
  }

  var manager = new THREE.LoadingManager( onModelLoaded );
  manager.onProgress = ( item, loaded, total ) => console.log( item, loaded, total );

  var loader = new THREE.OBJLoader( manager );
  loader.load( 'https://assets.codepen.io/557388/1405+Plane_1.obj', function ( obj ) { object = obj; });
}

function setupAnimation(model)
{
  let scene = new Scene(model);
  let plane = scene.modelGroup;
  
  gsap.fromTo('canvas',{x: "50%", autoAlpha: 0},  {duration: 1, x: "0%", autoAlpha: 1});
  gsap.to('.loading', {autoAlpha: 0})
  gsap.to('.scroll-cta', {opacity: 1})
  gsap.set('svg', {autoAlpha: 1})
  
  let tau = Math.PI * 2;

  gsap.set(plane.rotation, {y: tau * -.25});
  gsap.set(plane.position, {x: 80, y: -32, z: -60});
  
  scene.render();
  
  var sectionDuration = 1;
  gsap.fromTo(scene.views[1], 
    {   height: 1, bottom: 0 }, 
    {
      height: 0, bottom: 1,
      ease: 'none',
      scrollTrigger: {
        trigger: ".blueprint",
        scrub: true,
        start: "bottom bottom",
        end: "bottom top"
      }
    })
  
  gsap.fromTo(scene.views[1], 
    {   height: 0, bottom: 0 }, 
    {
      height: 1, bottom: 0,
      ease: 'none',
      scrollTrigger: {
        trigger: ".blueprint",
        scrub: true,
        start: "top bottom",
        end: "top top"
      }
    })
  
  gsap.to('.ground', {
    y: "30%",
    scrollTrigger: {
      trigger: ".ground-container",
      scrub: true,
      start: "top bottom",
      end: "bottom top"
    }
  })
  
  gsap.from('.clouds', {
    y: "25%",
    scrollTrigger: {
      trigger: ".ground-container",
      scrub: true,
      start: "top bottom",
      end: "bottom top"
    }
  })
  
  gsap.to('#line-length', {
    drawSVG: 100,
    scrollTrigger: {
      trigger: ".length",
      scrub: true,
      start: "top bottom",
      end: "top top"
    }
  })
  
  gsap.to('#line-wingspan', {
    drawSVG: 100,
    scrollTrigger: {
      trigger: ".wingspan",
      scrub: true,
      start: "top 25%",
      end: "bottom 50%"
    }
  })  
  
  gsap.to('#circle-phalange', {
    drawSVG: 100,
    scrollTrigger: {
      trigger: ".phalange",
      scrub: true,
      start: "top 50%",
      end: "bottom 100%"
    }
  })
  
  gsap.to('#line-length', {
    opacity: 0,
    drawSVG: 0,
    scrollTrigger: {
      trigger: ".length",
      scrub: true,
      start: "top top",
      end: "bottom top"
    }
  })
  
  gsap.to('#line-wingspan', {
    opacity: 0,
    drawSVG: 0,
    scrollTrigger: {
      trigger: ".wingspan",
      scrub: true,
      start: "top top",
      end: "bottom top"
    }
  })  
  
  gsap.to('#circle-phalange', {
    opacity: 0,
    drawSVG: 0,
    scrollTrigger: {
      trigger: ".phalange",
      scrub: true,
      start: "top top",
      end: "bottom top"
    }
  })
  
  let tl = new gsap.timeline(
  {
    onUpdate: scene.render,
    scrollTrigger: {
      trigger: ".content",
      scrub: true,
      start: "top top",
      end: "bottom bottom"
    },
    defaults: {duration: sectionDuration, ease: 'power2.inOut'}
  });
  
  let delay = 0;
  
  tl.to('.scroll-cta', {duration: 0.25, opacity: 0}, delay)
  tl.to(plane.position, {x: -10, ease: 'power1.in'}, delay)
  
  delay += sectionDuration;
  
  tl.to(plane.rotation, {x: tau * .25, y: 0, z: -tau * 0.05, ease: 'power1.inOut'}, delay)
  tl.to(plane.position, {x: -40, y: 0, z: -60, ease: 'power1.inOut'}, delay)
  
  delay += sectionDuration;
  
  tl.to(plane.rotation, {x: tau * .25, y: 0,  z: tau * 0.05, ease: 'power3.inOut'}, delay)
  tl.to(plane.position, {x: 40, y: 0, z: -60, ease: 'power2.inOut'}, delay)
  
  delay += sectionDuration;
  
  tl.to(plane.rotation, {x: tau * .2, y: 0, z: -tau * 0.1, ease: 'power3.inOut'}, delay)
  tl.to(plane.position, {x: -40, y: 0, z: -30, ease: 'power2.inOut'}, delay)
  
  delay += sectionDuration;
  
  tl.to(plane.rotation, { x: 0, z: 0, y: tau * .25}, delay)
  tl.to(plane.position, { x: 0, y: -10, z: 50}, delay)
  
  delay += sectionDuration;
  delay += sectionDuration;
  
  tl.to(plane.rotation, {x: tau * 0.25, y: tau *.5, z: 0, ease:'power4.inOut'}, delay)
  tl.to(plane.position, {z: 30, ease:'power4.inOut'}, delay)
  
  delay += sectionDuration;
  
  tl.to(plane.rotation, {x: tau * 0.25, y: tau *.5, z: 0, ease:'power4.inOut'}, delay)
  tl.to(plane.position, {z: 60, x: 30, ease:'power4.inOut'}, delay)
  
  delay += sectionDuration;
  
  tl.to(plane.rotation, {x: tau * 0.35, y: tau *.75, z: tau * 0.6, ease:'power4.inOut'}, delay)
  tl.to(plane.position, {z: 100, x: 20, y: 0, ease:'power4.inOut'}, delay)
  
  delay += sectionDuration;
  
  tl.to(plane.rotation, {x: tau * 0.15, y: tau *.85, z: -tau * 0, ease: 'power1.in'}, delay)
  tl.to(plane.position, {z: -150, x: 0, y: 0, ease: 'power1.inOut'}, delay)
  
  delay += sectionDuration;
  
  tl.to(plane.rotation, {duration: sectionDuration, x: -tau * 0.05, y: tau, z: -tau * 0.1, ease: 'none'}, delay)
  tl.to(plane.position, {duration: sectionDuration, x: 0, y: 30, z: 320, ease: 'power1.in'}, delay)
  
  tl.to(scene.light.position, {duration: sectionDuration, x: 0, y: 0, z: 0}, delay)
}

loadModel();
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://codepen.io/steveg3003/pen/zBVakw.js
  2. https://unpkg.com/three@0.117.1/build/three.min.js
  3. https://unpkg.com/three@0.117.1/examples/js/loaders/OBJLoader.js
  4. https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.1/gsap.min.js
  5. https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.1/ScrollTrigger.min.js
  6. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/DrawSVGPlugin3.min.js