ThreeJS | Animate a line on scroll and make the camera follow it.
New here? Learn about Bountify and follow @bountify to get notified of new bounties! x

I have a Float32Array of points (x,y,z) coordinates of where the line should be drawn.

In a threejs scene, I need you draw a line using those points, and then animate it on scroll from start position to end position.

The camera should also follow along the line as it animates.

To clarity, it doesn't have to be a line object, it's okay if it's geometry. Bonus points if I can configure a shape (eg not just a "tube", but a flat plane (like a road) etc.
I think the spline example from three.js documentation is a good place to start?

For context, I'm trying to recreate this in 3D: https://tympanus.net/Development/StorytellingMap/

This is what I already have: https://imgur.com/a/G56nd69

Important is that everything is well-commented, so I can implement into my own project.

Bonus points if you help me implement it within my own codebase at https://github.com/elfensky/storymap

awarded to jamesless1010

Crowdsource coding tasks.

1 Solution

Winning solution

This is using an older version of your project so I can't do a pull request right now, but it works and the line is a geometric tube so you can change the thickness.

import { GLOBAL as $ } from './globals'
import * as THREE from 'three'
import * as GEOLIB from 'geolib'
import { Vector3 } from 'three'
import { Vector2 } from 'three'
let clock = new THREE.Clock()

//on scroll event listener. well, it's not scroll but just mousewheel, you get the idea though.
//can replace it with 'scroll'
document.addEventListener('mousewheel', (event) => {
    $.drawRange += event.deltaY / 1000
    if ($.drawRange < 2) $.drawRange = 2
})

//
async function animatePath() {
    $.data = await loadPath()
    $.lineArray = new Float32Array(($.data.length + 1) * 3)

    let drawRange = 0

    $.data.forEach((element, index) => {
        $.lineArray[index * 3 + 0] = element[0]
        $.lineArray[index * 3 + 1] = element[1]
        $.lineArray[index * 3 + 2] = element[2]
        drawRange += 1
    })

    $.camera.up = new THREE.Vector3(0, 1, 0)
    drawPath()
}

async function loadPath() {
    return await fetch($.config.path).then((response) => {
        return response.json().then((data) => {
            return data
        })
    })
}

// this is exported and imported in the initiate.js and put inside the render function.
//basically, recreates the tubegeometry and updates it based on the event listener on top of this class
function updatePath() {
    drawPath()
}

//this (re)drawns the tube. Instead of just using a WebGL line I used a Tube, so you can change the thickness.
function drawPath() {
    if ($.line) {
        $.scene.remove($.line)
    }
    if ($.lineArray.length == 0) return
    if ($.drawRange <= 0) return
    let drawRange = Math.floor($.drawRange) * 3

    let diff = $.drawRange - Math.floor($.drawRange)
    let copyLineArray = []

    if (diff > 0 && drawRange >= 3 && drawRange <= $.lineArray.length - 3) {
        let x1 = $.lineArray[drawRange - 3]
        let y1 = $.lineArray[drawRange - 2]
        let z1 = $.lineArray[drawRange - 1]
        let x2 = $.lineArray[drawRange]
        let y2 = $.lineArray[drawRange + 1]
        let z2 = $.lineArray[drawRange + 2]

        let dis = new Vector3((x2 - x1) * diff + x1, (y2 - y1) * diff + y1, (z2 - z1) * diff + z1)
        copyLineArray = new Float32Array(drawRange + 3)
        for (var i = 0; i < drawRange; i++) copyLineArray[i] = $.lineArray[i]
        copyLineArray[i] = dis.x
        copyLineArray[i + 1] = dis.y
        copyLineArray[i + 2] = dis.z
    } else {
        copyLineArray = $.lineArray.filter((a, i) => i < drawRange)
    }

    if (drawRange >= 3) {
        let xLenth = copyLineArray.length
        $.camera.lookAt(
            copyLineArray[xLenth - 3],
            copyLineArray[xLenth - 2],
            copyLineArray[xLenth - 1],
        )
        $.camera.position.set(
            copyLineArray[xLenth - 3] + 2,
            copyLineArray[xLenth - 2] + 2,
            copyLineArray[xLenth - 1],
        )
    }

    let vecs = []
    for (var i = 0; i < copyLineArray.length; i += 3)
        vecs.push(new Vector3(copyLineArray[i], copyLineArray[i + 1], copyLineArray[i + 2]))

    const path = new THREE.CatmullRomCurve3(vecs)

    var geometry = new THREE.TubeGeometry(path, 100, 0.01, 100, false)
... (12 lines left)
Collapse
ANSWER.md
4 KB
This is using an older version of your project so I can't do a pull request right now, but it works and the line is a geometric tube so you can change the thickness.

```js
import { GLOBAL as $ } from './globals'
import * as THREE from 'three'
import * as GEOLIB from 'geolib'
import { Vector3 } from 'three'
import { Vector2 } from 'three'
let clock = new THREE.Clock()

//on scroll event listener. well, it's not scroll but just mousewheel, you get the idea though.
//can replace it with 'scroll'
document.addEventListener('mousewheel', (event) => {
    $.drawRange += event.deltaY / 1000
    if ($.drawRange < 2) $.drawRange = 2
})

//
async function animatePath() {
    $.data = await loadPath()
    $.lineArray = new Float32Array(($.data.length + 1) * 3)

    let drawRange = 0

    $.data.forEach((element, index) => {
        $.lineArray[index * 3 + 0] = element[0]
        $.lineArray[index * 3 + 1] = element[1]
        $.lineArray[index * 3 + 2] = element[2]
        drawRange += 1
    })

    $.camera.up = new THREE.Vector3(0, 1, 0)
    drawPath()
}

async function loadPath() {
    return await fetch($.config.path).then((response) => {
        return response.json().then((data) => {
            return data
        })
    })
}

// this is exported and imported in the initiate.js and put inside the render function.
//basically, recreates the tubegeometry and updates it based on the event listener on top of this class
function updatePath() {
    drawPath()
}

//this (re)drawns the tube. Instead of just using a WebGL line I used a Tube, so you can change the thickness.
function drawPath() {
    if ($.line) {
        $.scene.remove($.line)
    }
    if ($.lineArray.length == 0) return
    if ($.drawRange <= 0) return
    let drawRange = Math.floor($.drawRange) * 3

    let diff = $.drawRange - Math.floor($.drawRange)
    let copyLineArray = []

    if (diff > 0 && drawRange >= 3 && drawRange <= $.lineArray.length - 3) {
        let x1 = $.lineArray[drawRange - 3]
        let y1 = $.lineArray[drawRange - 2]
        let z1 = $.lineArray[drawRange - 1]
        let x2 = $.lineArray[drawRange]
        let y2 = $.lineArray[drawRange + 1]
        let z2 = $.lineArray[drawRange + 2]

        let dis = new Vector3((x2 - x1) * diff + x1, (y2 - y1) * diff + y1, (z2 - z1) * diff + z1)
        copyLineArray = new Float32Array(drawRange + 3)
        for (var i = 0; i < drawRange; i++) copyLineArray[i] = $.lineArray[i]
        copyLineArray[i] = dis.x
        copyLineArray[i + 1] = dis.y
        copyLineArray[i + 2] = dis.z
    } else {
        copyLineArray = $.lineArray.filter((a, i) => i < drawRange)
    }

    if (drawRange >= 3) {
        let xLenth = copyLineArray.length
        $.camera.lookAt(
            copyLineArray[xLenth - 3],
            copyLineArray[xLenth - 2],
            copyLineArray[xLenth - 1],
        )
        $.camera.position.set(
            copyLineArray[xLenth - 3] + 2,
            copyLineArray[xLenth - 2] + 2,
            copyLineArray[xLenth - 1],
        )
    }

    let vecs = []
    for (var i = 0; i < copyLineArray.length; i += 3)
        vecs.push(new Vector3(copyLineArray[i], copyLineArray[i + 1], copyLineArray[i + 2]))

    const path = new THREE.CatmullRomCurve3(vecs)

    var geometry = new THREE.TubeGeometry(path, 100, 0.01, 100, false)

    let material = new THREE.MeshNormalMaterial({
        flatShading: true,
    })

    $.line = new THREE.Mesh(geometry, material)
    $.scene.add($.line)
}

export { animatePath, updatePath }
Can you send me a zip with the full project so I can easily test it?
elfensky 4 months ago
Sure, here you go: https://we.tl/t-fBt7ngxuUB
jamesless1010 4 months ago