<div class="container" id="webgl"></div>
html, body
{
    margin: 0;
    padding: 0;
    overflow: hidden;
    width: 100%;
    height: 100%;
}

.container {
    width: 100%;
    height: 100%;
	background-color: black;
}
View Compiled
console.clear();

class Stage {

  constructor(mount) {

    this.container = mount;
    this.lines = [];
      
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color( 'black' );
    
    this.tickFunctions = [];

    this.size = {
      width: 1,
      height: 1
    }

    this.setupCamera();
    this.setupRenderer();
    this.onResize();
    window.addEventListener('resize', () => this.onResize());
    
    this.tick();
  }

  setupCamera() {

    this.lookAt = new THREE.Vector3(0, 0, 0);
    this.camera = new THREE.PerspectiveCamera(40, this.size.width / this.size.height, 0.1, 150);
    this.camera.position.set(0, 80, 100);
    this.camera.home = {
      position: { ...this.camera.position }
    }
    
    this.add(this.camera);
  }

  setupRenderer() {

    this.renderer = new THREE.WebGLRenderer({
      canvas: this.canvas,
      antialias: true,
      
    })
    
	  this.container.appendChild( this.renderer.domElement );
  }

  onResize() {
  
    this.size.width = this.container.clientWidth;
    this.size.height = this.container.clientHeight;
    
    this.camera.aspect = this.size.width / this.size.height
    this.camera.updateProjectionMatrix()

    this.renderer.setSize(this.size.width, this.size.height)
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  }

  addTickFunction(tickFunction)
  {
    this.tickFunctions.push(tickFunction)
  }

  tick() {
    this.camera.lookAt(this.lookAt);
    this.tickFunctions.forEach(func => func())
    this.renderer.render(this.scene, this.camera);
    window.requestAnimationFrame(() => this.tick())
  }

  add(element) { this.scene.add(element);}

  destroy() {

    this.container.removeChild( this.renderer.domElement);
    window.removeEventListener('resize', this.onResize);
  }
}

class Line 
{
  constructor(width, color, radius, near, far) {
    this.color = color;
    this.width = width;
    this.radius = radius;
    this.near = near;
    this.far = far;
    this.dashLength = 5 ;
    this.travelDistance = 1;
    this.mesh = null;
    this.createLine()
  }

  createLine()
  {
    const points = [];
    var angle = Math.random() * TAU
    this.radius += Math.random() * 0.1

    let pos = {
      x: Math.cos(angle) * this.radius, 
      y: 0,
      z: Math.sin(angle) * this.radius
    }

    let direction = angle + 1 + (Math.random() * 0.1);
    let y = 0;

    const steps = 75;

     for (let j = 0; j < steps; j ++) {
   
      direction += (0.012 * ((steps - j) * 0.1)) + (-0.4 + Math.random() * 0.8 ) * (j * (Math.random() * 0.02));
      y += (-1 + Math.random() * 2)  * (j * 0.001);

      pos.x += Math.cos(direction) * this.travelDistance 
      pos.y += Math.sin(y) * this.travelDistance
      pos.z += Math.sin(direction) * this.travelDistance

      points.push(pos.x, pos.y, pos.z);
    }

    const line = new MeshLine();
    line.setPoints(points,  p => this.width * Math.pow(1 - Math.cos(TAU * p), 0.3)  );

    const material = new MeshLineMaterial({
      color: new THREE.Color(this.color),
      transparent: true,
      depthTest: false,
      dashArray: this.dashLength,
      dashOffset: 0,
      dashRatio: 0.92
    });

    this.mesh = new THREE.Mesh(line, material);

    setTimeout(() => {
      const delayedStart = Math.random() > 0.3;
      if(delayedStart) setTimeout(() => this.animate(), 100 + Math.random() * 4000)
      else this.animate(false)
    }, 1000)
  }

  animate(slow = true) 
  {
    const duration = (slow ? 4 : 3) + Math.random() * 1;
    const ease = 'power4.inOut';
    const brightness = slow ? .8 : 1;

    gsap.fromTo(this.mesh.material.uniforms.dashOffset, 
      { value: 0 },
      {
        value: -this.dashLength * 0.3, 
        duration, 
        ease,
        onComplete: () => setTimeout(() => this.animate(), Math.random() * 4000)
      })
    const color = this.mesh.material.uniforms.color.value;
    gsap.fromTo(color, { r: 0, g: 0, b: 0, }, {
      motionPath: [
        {
          r: color.r + brightness, 
          g: color.g + brightness, 
          b: color.b + brightness, 
        },
        {
          r: color.r, 
          g: color.g, 
          b: color.b, 
        }
      ],
      duration: duration * 0.5, 
      ease: 'power2.in'
    })
  }
}

const TAU = Math.PI * 2;
const colors = ["#f94144","#f3722c","#f8961e","#f9844a","#f9c74f","#90be6d","#43aa8b","#4d908e","#577590","#277da1"]

const canvas = document.getElementById('webgl');
const stage = new Stage(canvas);

const lineGroup = new THREE.Group();
lineGroup.rotation.y = Math.PI
stage.add(lineGroup);

for (let i = 0; i < 600; i++) {

  const lineThickness = 0.1 + Math.random() * 0.1;
  const startRadius = 2 + Math.random() * 0.5;
  const lineColor = colors[Math.floor(Math.random() * colors.length)];

  const line = new Line(
    lineThickness,
    lineColor,
    startRadius,
    stage.camera.near,
    stage.camera.far
  )
  lineGroup.add(line.mesh);
  stage.lines.push({
    obj: line,
    speed: 0.002 + Math.random() * 0.009
  });
}

const tick = () => {
  lineGroup.rotation.y += 0.01;
  lineGroup.rotation.x = Math.cos(lineGroup.rotation.y * 1.2) * 0.5;
  lineGroup.rotation.z = Math.sin(lineGroup.rotation.y) * 0.01;
}

stage.addTickFunction(tick)
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://codepen.io/steveg3003/pen/zBVakw
  2. https://cdnjs.cloudflare.com/ajax/libs/three.js/r126/three.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/three.meshline/1.3.0/THREE.MeshLine.js
  4. https://unpkg.co/gsap@3/dist/gsap.min.js
  5. https://unpkg.com/gsap@3/dist/MotionPathPlugin.min.js