<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
Run Pen

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