Search code examples
javascriptanimationthree.jsgsap

Three.js TimeLineLite animation not working


I'm trying to animate the TextGeometries to form a shape of box {delay} sphere {delay} and then a cone. Also, change the colors while the transition. I tried the below code for trying to animate the initial state to a box:

var pointsInsideShape = shapes[0].points.attributes.position.array;
for (i = 0; i < MAX_PARTICLES; i++) {
  var tl = new TimelineLite();
  tl.from(particleGroup.children[i].position, 2, {
    x: pointsInsideShape[ index ++ ],
    y: pointsInsideShape[ index ++ ],
    z: pointsInsideShape[ index ++ ]
  })
}

The console throws the error below:

particleGroup.children[i] is undefined

I checked the particleGroup has objects, however I'm getting this error. I'm not sure why this isn't working.

Currently all the particles are in the center, I'm trying to animate them to form a shape of a sphere, then a delay before the next shape animation and so on. Repeat the loop at the end. If someone could point me in the right direction, it'd be much appreciated. Thank you!

Below is the code:

var renderer, camera, scene, light, shapes, triggers, particleCount, particleGroup,
  defaultAnimationSpeed, morphAnimationSpeed, colorToStartWith, textGeometries, loader, typeface,
  animationVars;

const MAX_PARTICLES = 100,
  PARTICLE_SIZE = .65;

var sts = {
  config: function() {
    shapes = [{
        "geoCode": new THREE.BoxBufferGeometry(13, 13, 13),
        "textMat": new THREE.MeshPhongMaterial({
          color: 0x029894
        }),
        "color": 0x029894,
        "size": [50, 50, 50]
      },
      {
        "geoCode": new THREE.SphereBufferGeometry(25, 33, 33),
        "textMat": new THREE.MeshPhongMaterial({
          color: 0x8F3985
        }),
        "color": 0x8F3985,
        "size": [25, 33, 33]
      },
      {
        "geoCode": new THREE.ConeBufferGeometry(25, 50, 30),
        "textMat": new THREE.MeshPhongMaterial({
          color: 0x11659C
        }),
        "color": 0x11659C,
        "size": [25, 50, 30]
      }
    ];
    triggers = document.getElementsByTagName('span');
    particleGroup = new THREE.Group();
    defaultAnimationSpeed = 1;
    morphAnimationSpeed = 18;
    normalSpeed = (defaultAnimationSpeed / 100)
    fullSpeed = (morphAnimationSpeed / 100)
    colorToStartWith = '#8F3985';
    textGeometries = new Array();
  },
  initScene: function() {

    //Renderer
    renderer = new THREE.WebGLRenderer({
      antialias: true
    });
    renderer.setClearColor(0xffffff, 1);
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

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

    // Camera and position
    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000);
    camera.position.y = -25;
    camera.position.z = 45;
    camera.rotation.x = -.45;

    // Lighting
    light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(1, 1, 1).normalize();
    scene.add(light);
    var light2 = new THREE.DirectionalLight(0xffffff, 1.5);
    light2.position.set(0, -1, 0);
    scene.add(light2);

    // Texts
    loader = new THREE.FontLoader();
    typeface = 'https://raw.githubusercontent.com/7dir/json-fonts/master/fonts/cyrillic/roboto/Roboto_Regular.json';

    //particleGroup
    particleGroup = new THREE.Group();
    particleGroup.position.x = 0;
    particleGroup.position.y = -45;
    particleGroup.position.z = 0;
    scene.add(particleGroup);
  },
  fullScreen: function() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  },
  attachEvents: function() {
    window.addEventListener('resize', this.fullScreen, false);
  },
  addShapes: function() {
    for (idx = 0; idx < shapes.length; idx++) {
      shapes[idx].geometry = shapes[idx].geoCode.toNonIndexed();
      m = new THREE.MeshLambertMaterial({
        color: shapes[idx].color,
        opacity: 0,
        transparent: true,
        wireframe: true
      });
      shapes[idx].geometry.center();
      shape = new THREE.Mesh(shapes[idx].geometry, m);
      scene.add(shape);
    }
  },
  computePointsInside: function(idx) {
    shapes[idx].points = this.fillWithPoints(shapes[idx].geometry, MAX_PARTICLES);
  },
  addText: function() {
    loader.load(typeface, (font) => {
      //var x, y, z, index1, index2;
      //x = y = z = index1 = index2 = 0;
      var m = new THREE.MeshPhongMaterial({
        color: 0x8F3985,
        opacity: .8,
        specular: 0xffffff,
        shininess: 100
      });
      for (i = 0; i < MAX_PARTICLES; i++) {
        g = new THREE.TextGeometry(Math.random() < .5 ? '6' : '9', {
          font: font,
          size: PARTICLE_SIZE,
          height: 0.1
        });

        var text = new THREE.Mesh(g, m);
        text.position.x = 0;
        text.position.y = 0;
        text.position.z = 0;

        text.rotation.x = Math.random() * 2 * Math.PI;
        text.rotation.y = Math.random() * 2 * Math.PI;
        text.rotation.z = Math.random() * 2 * Math.PI;

        text.scale.x = (Math.random() * (0.95 - 0.1) + 0.95).toFixed(4);
        text.scale.y = (Math.random() * (0.95 - 0.1) + 0.95).toFixed(4);
        text.scale.z = (Math.random() * (0.95 - 0.1) + 0.95).toFixed(4);

        //var pointsInsideShape = shapes[0].points.attributes.position.array;

        //text.position.x = pointsInsideShape[ index2 ++ ];
        //text.position.y = pointsInsideShape[ index2 ++ ];
        //text.position.z = pointsInsideShape[ index2 ++ ];

        particleGroup.add(text);
      }
    });
  },
  animationSequence: function() {
    var x, y, z, index;
    x = y = z = index = 0;
    var pointsInsideShape = shapes[0].points.attributes.position.array;
    for (i = 0; i < MAX_PARTICLES; i++) {
      var tl = new TimelineLite();
      tl.from(particleGroup.children[i].position, 2, {
        x: pointsInsideShape[index++],
        y: pointsInsideShape[index++],
        z: pointsInsideShape[index++]
      })
    }
  },
  animate: function() {
    window.requestAnimationFrame(sts.animate);
    particleGroup.rotation.y += 0.005;
    renderer.render(scene, camera)
  },
  fillWithPoints: function(geometry, count) {

    var ray = new THREE.Ray()

    var size = new THREE.Vector3();
    geometry.computeBoundingBox();
    var bbox = geometry.boundingBox;

    var points = [];

    var dir = new THREE.Vector3(1, 1, 1).normalize();
    for (var i = 0; i < count; i++) {
      var p = setRandomVector(bbox.min, bbox.max);
      points.push(p);
    }

    function setRandomVector(min, max) {
      var v = new THREE.Vector3(
        THREE.Math.randFloat(min.x, max.x),
        THREE.Math.randFloat(min.y, max.y),
        THREE.Math.randFloat(min.z, max.z)
      );
      if (!isInside(v)) {
        return setRandomVector(min, max);
      }
      return v;
    }

    function isInside(v) {

      ray.set(v, dir);
      var counter = 0;

      var pos = geometry.attributes.position;
      var faces = pos.count / 3;
      var vA = new THREE.Vector3(),
        vB = new THREE.Vector3(),
        vC = new THREE.Vector3();

      for (var i = 0; i < faces; i++) {
        vA.fromBufferAttribute(pos, i * 3 + 0);
        vB.fromBufferAttribute(pos, i * 3 + 1);
        vC.fromBufferAttribute(pos, i * 3 + 2);
        if (ray.intersectTriangle(vA, vB, vC)) counter++;
      }

      return counter % 2 == 1;
    }

    return new THREE.BufferGeometry().setFromPoints(points);
  },
  init: function() {
    this.config();
    this.initScene();
    this.attachEvents();
    this.addShapes();
    this.computePointsInside(0);
    this.computePointsInside(1);
    this.computePointsInside(2);
    this.addText();
    this.animate();
    this.animationSequence();
  }
}

sts.init();
body {
  margin: 0;
  overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js" type="text/javascript"></script>


Solution

  • I added a slight delay to animationSequence

    setTimeout(() => this.animationSequence(), 1000);
    

    This is not the best approach and ideally you'd wait until particleGroup.children is loaded with data

    After bit of investigation I found the culprit.

    addText: function() {
        loader.load(typeface, (font) => {
    

    Wrap that in Promise

    addText: function() {
        return new Promise((resolve, reject) => {
        loader.load(typeface, (font) => {
    

    Use put rest of logic in then callback

    this.addText().then(_ => {
          this.animate();
          this.animationSequence();
        })
    

    Working example

    var renderer, camera, scene, light, shapes, triggers, particleCount, particleGroup,
    defaultAnimationSpeed, morphAnimationSpeed, colorToStartWith, textGeometries, loader, typeface,
    animationVars;
    
    const MAX_PARTICLES = 100, PARTICLE_SIZE = .65;
    
    var sts = {
      config: function() {
        shapes = [
          {
            "geoCode": new THREE.BoxBufferGeometry(13, 13, 13),
            "textMat": new THREE.MeshPhongMaterial({ color: 0x029894 }),
            "color": 0x029894,
            "size": [50, 50, 50]
          },
          {
            "geoCode": new THREE.SphereBufferGeometry(25, 33, 33),
            "textMat": new THREE.MeshPhongMaterial({ color: 0x8F3985 }),
            "color": 0x8F3985,
            "size": [25, 33, 33]
          },
          {
            "geoCode": new THREE.ConeBufferGeometry(25, 50, 30),
            "textMat": new THREE.MeshPhongMaterial({ color: 0x11659C }),
            "color": 0x11659C,
            "size": [25, 50, 30]
          }
        ];
        triggers = document.getElementsByTagName('span');
        particleGroup = new THREE.Group();
        defaultAnimationSpeed = 1;
        morphAnimationSpeed = 18;
        normalSpeed = (defaultAnimationSpeed / 100),
        fullSpeed = (morphAnimationSpeed / 100)
        colorToStartWith = '#8F3985';
        textGeometries = new Array();
      },
      initScene: function() {
    
        //Renderer
        renderer = new THREE.WebGLRenderer({antialias: true});
        renderer.setClearColor(0xffffff, 1);
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
    
        // Scene
        scene = new THREE.Scene();
    
        // Camera and position
        camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.y = -25;
        camera.position.z = 45;
        camera.rotation.x = -.45;
    
        // Lighting
        light = new THREE.DirectionalLight( 0xffffff, 1 );
    		light.position.set( 1, 1, 1 ).normalize();
        scene.add(light);
    		var light2 = new THREE.DirectionalLight( 0xffffff, 1.5 );
    		light2.position.set( 0, - 1, 0 );
    		scene.add( light2 );
    
        // Texts
        loader = new THREE.FontLoader();
        typeface = 'https://raw.githubusercontent.com/7dir/json-fonts/master/fonts/cyrillic/roboto/Roboto_Regular.json';
    
        //particleGroup
        particleGroup = new THREE.Group();
        particleGroup.position.x = 0;
        particleGroup.position.y = -45;
        particleGroup.position.z = 0;
        scene.add(particleGroup);
      },
      fullScreen: function() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
      },
      attachEvents: function() {
        window.addEventListener('resize', this.fullScreen, false);
      },
      addShapes: function() {
        for (idx = 0; idx < shapes.length; idx++) {
          shapes[idx].geometry = shapes[idx].geoCode.toNonIndexed();
          m = new THREE.MeshLambertMaterial({
            color: shapes[idx].color,
            opacity: 0,
            transparent: true,
            wireframe: true
          });
          shapes[idx].geometry.center();
          shape = new THREE.Mesh(shapes[idx].geometry, m);
          scene.add(shape);
        }
      },
      computePointsInside: function(idx) {
        shapes[idx].points = this.fillWithPoints(shapes[idx].geometry, MAX_PARTICLES);
      },
      addText: function() {
        return new Promise((resolve, reject) => {
          loader.load(typeface, (font) => {
            //var x, y, z, index1, index2;
            //x = y = z = index1 = index2 = 0;
            var m = new THREE.MeshPhongMaterial({color: 0x8F3985, opacity: .8, specular: 0xffffff, shininess: 100});
            for (i = 0; i < MAX_PARTICLES; i++) {
              g = new THREE.TextGeometry( Math.random() < .5 ? '6' : '9', {
                font: font,
                size: PARTICLE_SIZE,
                height: 0.1
              });
    
              var text = new THREE.Mesh(g, m);
              text.position.x = 0;
              text.position.y = 0;
              text.position.z = 0;
    
              text.rotation.x = Math.random() * 2 * Math.PI;
              text.rotation.y = Math.random() * 2 * Math.PI;
              text.rotation.z = Math.random() * 2 * Math.PI;
    
              text.scale.x = (Math.random() * (0.95 - 0.1) + 0.95).toFixed(4);
              text.scale.y = (Math.random() * (0.95 - 0.1) + 0.95).toFixed(4);
              text.scale.z = (Math.random() * (0.95 - 0.1) + 0.95).toFixed(4);
    
              //var pointsInsideShape = shapes[0].points.attributes.position.array;
    
              //text.position.x = pointsInsideShape[ index2 ++ ];
              //text.position.y = pointsInsideShape[ index2 ++ ];
              //text.position.z = pointsInsideShape[ index2 ++ ];
    
              particleGroup.add(text);
            }
            resolve();
          });
        })
      },
      animationSequence: function() {
        var x, y, z, index;
        x = y = z = index = 0;
        
        const children = particleGroup.children;
        
        var pointsInsideShape = shapes[0].points.attributes.position.array;
        for (i = 0; i < MAX_PARTICLES; i++) {
          var tl = new TimelineLite();
          const child = children[i];
          
          tl.from(child.position, 2, {
            x: pointsInsideShape[ index ++ ],
            y: pointsInsideShape[ index ++ ],
            z: pointsInsideShape[ index ++ ]
          })
        }
      },
      animate: function() {
        window.requestAnimationFrame(sts.animate);
        particleGroup.rotation.y +=0.005;
        renderer.render(scene, camera)
      },
      fillWithPoints: function(geometry, count) {
        
        var ray = new THREE.Ray()
        
        var size = new THREE.Vector3();
        geometry.computeBoundingBox();
        var bbox = geometry.boundingBox;
        
        var points = [];
        
        var dir = new THREE.Vector3(1, 1, 1).normalize();
        for (var i = 0; i < count; i++) {
          var p = setRandomVector(bbox.min, bbox.max);
          points.push(p);
        }
        
        function setRandomVector(min, max){
          var v = new THREE.Vector3(
            THREE.Math.randFloat(min.x, max.x),
            THREE.Math.randFloat(min.y, max.y),
            THREE.Math.randFloat(min.z, max.z)
          );
          if (!isInside(v)){return setRandomVector(min, max);}
          return v;
        }
        
        function isInside(v){
          
          ray.set(v, dir);
          var counter = 0;
    
          var pos = geometry.attributes.position;
          var faces = pos.count / 3;
          var vA = new THREE.Vector3(), vB = new THREE.Vector3(), vC = new THREE.Vector3();
      
          for(var i = 0; i < faces; i++){
            vA.fromBufferAttribute(pos, i * 3 + 0);
            vB.fromBufferAttribute(pos, i * 3 + 1);
            vC.fromBufferAttribute(pos, i * 3 + 2);
            if (ray.intersectTriangle(vA, vB, vC)) counter++;
          }
          
          return counter % 2 == 1;
        }
        
        return new THREE.BufferGeometry().setFromPoints(points);
      },
      init: function() {
        this.config();
        this.initScene();
        this.attachEvents();
        this.addShapes();
        this.computePointsInside(0);
        this.computePointsInside(1);
        this.computePointsInside(2);
        this.addText().then(_ => {
          this.animate();
          this.animationSequence();
        })
      }
    }
    
    sts.init();
    body {
      margin: 0;
      overflow: hidden;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.js" type="text/javascript"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js" type="text/javascript"></script>