Created
January 6, 2025 20:44
-
-
Save thirdlf03/321e0d99f8fba43791f97cbe9eef49b1 to your computer and use it in GitHub Desktop.
three.js scroll based animation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @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