Three.js - Scroll Based Animation

Aus Wikizone
Wechseln zu: Navigation, Suche

Links[Bearbeiten]

ThreeJS

Snippets[Bearbeiten]

Check Scroll Position[Bearbeiten]

 let scrollY = window.scrollY
 window.addEventListener('scroll', () =>
 {
     scrollY = window.scrollY
 
     console.log(scrollY)
 })

Scroll objects in sync to html scroll[Bearbeiten]

// position objects
const objectsDistance = 4
mesh1.position.y = - objectsDistance * 0
mesh2.position.y = - objectsDistance * 1
mesh3.position.y = - objectsDistance * 2
//...
// Calc sizes (HINT-no resize event in this example)
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

//...
// Base camera
const camera = new THREE.PerspectiveCamera(35, sizes.width / sizes.height, 0.1, 100)
camera.position.z = 6
scene.add(camera)

// Control Scrolling
let scrollY = window.scrollY
window.addEventListener('scroll', () => { scrollY = window.scrollY })

// ...

// tick function
const tick = () =>
{
// ...
    // Animate camera
    camera.position.y = - scrollY / sizes.height * objectsDistance // scroll objects in sync to html
// ...
}

tick()

Cursor based Parallax[Bearbeiten]

// ...
/**
 * Camera
 */
// Group (used to apply camara parallax without interfering the camera scroll)
const cameraGroup = new THREE.Group()
scene.add(cameraGroup)

// Base camera
const camera = new THREE.PerspectiveCamera(35, sizes.width / sizes.height, 0.1, 100)
camera.position.z = 6
cameraGroup.add(camera)

// ...

const tick = () =>
{
    // Animate camera
    // scroll objects in sync to html
    camera.position.y = - scrollY / sizes.height * objectsDistance 

    // cameraGroup parallax on cursor movement
    const parallaxX = cursor.x
    const parallaxY = - cursor.y
    cameraGroup.position.x = parallaxX
    cameraGroup.position.y = parallaxY
}
// ...

Parallax with easing[Bearbeiten]

Variation von oben

// ...
/**
 * Animate
 */
const clock = new THREE.Clock()
let previousTime = 0

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()
    // we need deltaTime to calc correct timbased (not fps based) movement for our easing
    const deltaTime = elapsedTime - previousTime
    previousTime = elapsedTime
    
    //...

    // Animate camera
    // scroll objects in sync to html
    camera.position.y = - scrollY / sizes.height * objectsDistance 

    // camera parallax on cursor movement
    const parallaxX = cursor.x * 0.5
    const parallaxY = - cursor.y * 0.5
    //cameraGroup.position.x = parallaxX
    //cameraGroup.position.y = parallaxY
    // add some easing
    cameraGroup.position.x += (parallaxX - cameraGroup.position.x) * 5 * deltaTime
    cameraGroup.position.y += (parallaxY - cameraGroup.position.y) * 5 * deltaTime

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

Partikel für zusätzliche Räumlichkeit einsetzen[Bearbeiten]

/**
 * Particles
 */
const particlesCount = 200

const positions = new Float32Array(particlesCount * 3)
for(let i = 0; i < particlesCount; i++)
{
    positions[i * 3 + 0] = (Math.random() - 0.5) * 10
    positions[i * 3 + 1] = objectsDistance * 0.5 - Math.random() * objectsDistance * sectionMeshes.length
    positions[i * 3 + 2] = (Math.random() - 0.5) * 10
}

// Geometry
const particlesGeometry = new THREE.BufferGeometry()
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))

// Material
const particlesMaterial = new THREE.PointsMaterial({
    color: parameters.materialColor,
    sizeAttenuation: true,
    size: 0.03
})

// Points
const particles = new THREE.Points(particlesGeometry, particlesMaterial)
scene.add(particles)

Scroll Trigger[Bearbeiten]

Wir nutzen GSAP aber nur für die Animation, nicht für den Trigger:

/**
 * Sizes
 */
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight
    //...
})
// ...

/**
 * Scroll
 */
 let scrollY = window.scrollY
 let currentSection = 0

 window.addEventListener('scroll', () =>
 {
     scrollY = window.scrollY
     const newSection = Math.round(scrollY / sizes.height) //liefert 0,1,2...
     if(newSection != currentSection)
    {
        console.log('changed', currentSection)
        currentSection = newSection
        // Section Trigger
        gsap.to(
            sectionMeshes[currentSection].rotation,
            {
                duration: 1.5,
                ease: 'power2.inOut',
                x: '+=6',
                y: '+=3',
                z: '+=1.5'
            }
        )
    }
 })

Complete Example (three.js journey)[Bearbeiten]

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>21 - Scroll base animation</title>
</head>
<body>
    <canvas class="webgl"></canvas>

    <section class="section">
        <h1>My Portfolio</h1>
    </section>
    <section class="section">
        <h2>My projects</h2>
    </section>
    <section class="section">
        <h2>Contact me</h2>
    </section>
</body>
</html>

CSS

*
{
    margin: 0;
    padding: 0;
}

html,
body
{
    background: #1e1a20;
}

.webgl
{
    position: fixed;
    top: 0;
    left: 0;
    outline: none;
}


.section
{
    display: flex;
    align-items: center;
    height: 100vh;
    position: relative;
    font-family: 'Cabin', sans-serif;
    color: #ffeded;
    text-transform: uppercase;
    font-size: 7vmin;
    padding-left: 10%;
    padding-right: 10%;
}

section:nth-child(odd)
{
    justify-content: flex-end;
}

JavaScript

import './style.css'
import * as THREE from 'three'
import * as dat from 'lil-gui'
import gsap from 'gsap'

/**
 * Debug
 */
const gui = new dat.GUI()

const parameters = {
    materialColor: '#ffeded'
}

gui
    .addColor(parameters, 'materialColor')
    .onChange(() =>
    {
        material.color.set(parameters.materialColor)
        particlesMaterial.color.set(parameters.materialColor)
    })

/**
 * Base
 */
// Canvas
const canvas = document.querySelector('canvas.webgl')

// Scene
const scene = new THREE.Scene()

/**
 * Objects
 */

// Texture
const textureLoader = new THREE.TextureLoader()
const gradientTexture = textureLoader.load('textures/gradients/3.jpg')
gradientTexture.magFilter = THREE.NearestFilter // prevent interpolation of colors

// Material
const material = new THREE.MeshToonMaterial({ 
    color: parameters.materialColor,
    gradientMap: gradientTexture 
})

// Meshes
const mesh1 = new THREE.Mesh(
    new THREE.TorusGeometry(1, 0.4, 16, 60),
    material
)
const mesh2 = new THREE.Mesh(
    new THREE.ConeGeometry(1, 2, 32),
    material
)
const mesh3 = new THREE.Mesh(
    new THREE.TorusKnotGeometry(0.8, 0.35, 100, 16),
    material
)

scene.add(mesh1, mesh2, mesh3)

// position objects
const objectsDistance = 4
mesh1.position.y = - objectsDistance * 0
mesh2.position.y = - objectsDistance * 1
mesh3.position.y = - objectsDistance * 2
mesh1.position.x = 2
mesh2.position.x = - 2
mesh3.position.x = 2

const sectionMeshes = [ mesh1, mesh2, mesh3 ]

/**
 * Particles
 */
const particlesCount = 200

const positions = new Float32Array(particlesCount * 3)
for(let i = 0; i < particlesCount; i++)
{
    positions[i * 3 + 0] = (Math.random() - 0.5) * 10
    positions[i * 3 + 1] = objectsDistance * 0.5 - Math.random() * objectsDistance * sectionMeshes.length
    positions[i * 3 + 2] = (Math.random() - 0.5) * 10
}

// Geometry
const particlesGeometry = new THREE.BufferGeometry()
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))

// Material
const particlesMaterial = new THREE.PointsMaterial({
    color: parameters.materialColor,
    sizeAttenuation: true,
    size: 0.03
})

// Points
const particles = new THREE.Points(particlesGeometry, particlesMaterial)
scene.add(particles)

/**
 * Lights
 */
 const directionalLight = new THREE.DirectionalLight('#ffffff', 1)
 directionalLight.position.set(1, 1, 0)
 scene.add(directionalLight)

/**
 * Sizes
 */
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

/**
 * Camera
 */
// Group (used to apply camara parallax without interfering the camera scroll)
const cameraGroup = new THREE.Group()
scene.add(cameraGroup)

// Base camera
const camera = new THREE.PerspectiveCamera(35, sizes.width / sizes.height, 0.1, 100)
camera.position.z = 6
cameraGroup.add(camera)


/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    alpha: true
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

/**
 * Scroll
 */
 let scrollY = window.scrollY
 let currentSection = 0

 window.addEventListener('scroll', () =>
 {
     scrollY = window.scrollY
     const newSection = Math.round(scrollY / sizes.height)
     if(newSection != currentSection)
    {
        console.log('changed', currentSection)
        currentSection = newSection
        // Section Trigger
        gsap.to(
            sectionMeshes[currentSection].rotation,
            {
                duration: 1.5,
                ease: 'power2.inOut',
                x: '+=6',
                y: '+=3',
                z: '+=1.5'
            }
        )
    }
 })

 /**
 * Cursor
 */
const cursor = {}
cursor.x = 0
cursor.y = 0

window.addEventListener('mousemove', (event) =>
{
    cursor.x = event.clientX / sizes.width - 0.5
    cursor.y = event.clientY / sizes.height - 0.5

    //console.log(cursor)
})


/**
 * Animate
 */
const clock = new THREE.Clock()
let previousTime = 0

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()
    // we need deltaTime to calc correct timbased (not fps based) movement for our easing
    const deltaTime = elapsedTime - previousTime
    previousTime = elapsedTime

    // Animate meshes
    for(const mesh of sectionMeshes)
    {
        // we use deltaTime this time, because gsap rotation would not apply otherwise
        // mesh.rotation.x = elapsedTime * 0.1
        // mesh.rotation.y = elapsedTime * 0.12
        mesh.rotation.x += deltaTime * 0.1
        mesh.rotation.y += deltaTime * 0.12
    }

    // Animate camera
    // scroll objects in sync to html
    camera.position.y = - scrollY / sizes.height * objectsDistance 

    // camera parallax on cursor movement with easing
    const parallaxX = cursor.x * 0.5
    const parallaxY = - cursor.y * 0.5
    cameraGroup.position.x += (parallaxX - cameraGroup.position.x) * 5 * deltaTime
    cameraGroup.position.y += (parallaxY - cameraGroup.position.y) * 5 * deltaTime
    
    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

Magnetic Sections - Todo[Bearbeiten]