Skip to content

Instantly share code, notes, and snippets.

@thirdlf03
Created January 6, 2025 20:44
Show Gist options
  • Select an option

  • Save thirdlf03/321e0d99f8fba43791f97cbe9eef49b1 to your computer and use it in GitHub Desktop.

Select an option

Save thirdlf03/321e0d99f8fba43791f97cbe9eef49b1 to your computer and use it in GitHub Desktop.
three.js scroll based animation
<div class="content">
<div class="loading">Loading</div>
<div class="trigger"></div>
<div class="section">
<h1>Routes.</h1>
<h3>guide.</h3>
<p>Three.js scroll based animation</p>
<div class="scroll-cta">Scroll</div>
</div>
<div class="section right">
<h2>Three.js scroll based animation</h2>
</div>
<div class="ground-container">
<div class="parallax ground"></div>
<div class="section right">
<h2>Three.js scroll based animation</h2>
<p>Saaay what!?.</p>
</div>
<div class="section">
<h2>Three.js scroll based animation</h2>
<p>Three.js scroll based animation</p>
</div>
<div class="section right">
<h2>Three.js scroll based animation</h2>
<p>Three.js scroll based animation</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>Three.js scroll based animation</h2>
<p>Three.js scroll based animation</p>
</div>
<div class="section dark length">
<h2>Three.js scroll based animation</h2>
<p>Three.js scroll based animation</p>
</div>
<div class="section dark wingspan">
<h2>Three.js scroll based animation</h2>
<p>Three.js scroll based animation</p>
</div>
<div class="section dark phalange">
<h2>Three.js scroll based animation</h2>
<p>Three.js scroll based animation</p>
</div>
<div class="section dark">
<h2>Three.js scroll based animation</h2>
<p>Three.js scroll based animation</p>
</div>
</div>
<div class="sunset">
<div class="section"></div>
<div class="section end">
<h2>Finish.</h2>
</div>
</div>
</div>
console.clear();
let mixer = null;
let animations = [];
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 ].children[1].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);
this.tick();
}
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;
let clock = new THREE.Clock;
let time = clock.getDelta();
mixer && mixer.update(time);
this.renderer.render( this.scene, camera );
}
window.requestAnimationFrame(this.render);
}
tick = () => {
for ( var ii = 0; ii < this.views.length; ++ ii ) {
var view = this.views[ ii ];
var camera = view.camera;
let clock = new THREE.Clock;
let time = clock.getDelta();
mixer && mixer.update(time);
this.renderer.render( this.scene, camera );
}
window.requestAnimationFrame(this.tick);
}
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; });
var loader = new THREE.GLTFLoader(manager);
loader.load('https://dragonir.github.io/3d/static/media/Fox.c62e8daba521d5a6b50c.glb', mesh => {
// loader.load('https://raw.githubusercontent.com/baronwatts/models/master/robber.glb', mesh => {
if (mesh.scene) {
object = mesh.scene;
animations = mesh.animations;
let meshAnimation = animations[0];
mixer = new THREE.AnimationMixer(mesh.scene);
let animationClip = meshAnimation;
let clipAction = mixer.clipAction(animationClip).play();
animationClip = clipAction.getClip();
}
});
}
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();
<script src="https://unpkg.com/[email protected]/build/three.min.js"></script>
<script src="https://unpkg.com/[email protected]/examples/js/loaders/OBJLoader.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.1/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.1/ScrollTrigger.min.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/DrawSVGPlugin3.min.js"></script>
<script src="https://unpkg.com/[email protected]/examples/js/loaders/GLTFLoader.js"></script>
@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, 0.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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment