<div data-stage></div>
*,
*:before,
*:after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background-color: black;
}

canvas {
  display: block;
}
View Compiled
console.clear();
class App {
  constructor(opts) {
    this.opts = Object.assign({}, App.defaultOpts, opts);
    this.world = new World();
    this.init();
  }
  init() {
    this.threeEnvironment();
    window.requestAnimationFrame(this.animate.bind(this));
  }
  threeEnvironment() {
    const light = new Light();
    this.world.sceneAdd(light.ambient);
    this.world.sceneAdd(light.sun);
    const lights = lightBalls(this.world, light.lights);
    const composition = new Composition({
      sideLength: 10,
      amount: 15,
      radius: 6,
      thickness: 2,
      offset: 0.3
    });
    this.world.sceneAdd(composition.tubes);
  }
  animate() {
    this.world.renderer.render(this.world.scene, this.world.camera);
    window.requestAnimationFrame(this.animate.bind(this));
  }
}

App.defaultOpts = {
  debug: false
};
function lightBalls(world, meshes) {
  const radius = 12.4;
  const mainTl = new TimelineMax();
  meshes.forEach(function (group) {
    world.sceneAdd(group);
    createAnimation(group);
  });
  function createAnimation(group) {
    const tl = new TimelineMax({
      yoyo: true
    });
    tl
      .set(group.position, {
        x: THREE.Math.randInt(-2, 2) * radius + radius * 0.5,
        z: THREE.Math.randInt(-2, 2) * radius + radius * 0.5
      })
      .to(group.position, 2, {
        y: 18,
        ease: Linear.easeNone
      })
      .to(
        group.children[0],
        1.2,
        {
          intensity: 4.0,
          distance: 18,
          ease: Linear.easeNone
        },
        "-=1.2"
      );
    tl.paused(true);
    mainTl.to(
      tl,
      1.2,
      {
        progress: 1,
        ease: SlowMo.ease.config(0.0, 0.1, true),
        onComplete: createAnimation,
        onCompleteParams: [group],
        delay: THREE.Math.randFloat(0, 0.8)
      },
      mainTl.time()
    );
  }
}
class Light {
  constructor() {
    this.lights = [];
    this.ambient = null;
    this.sun = null;
    this.createLights();
    this.createAmbient();
    this.createSun();
  }
  createLights() {
    for (let i = 0; i < 3; i++) {
      const group = new THREE.Group();
      const light = new THREE.PointLight(0x28D2CB);
      light.intensity = 4.0;
      light.distance = 6;
      light.decay = 1.0;
      group.add(light);
      const geometry = new THREE.SphereBufferGeometry(2, 16, 16);
      const material = new THREE.MeshBasicMaterial({
        color: 0x28D2CB
      });
      const mesh = new THREE.Mesh(geometry, material);
      group.add(mesh);
      group.position.set(0, -5, 0);
      this.lights.push(group);
    }
  }
  createAmbient() {
    this.ambient = new THREE.AmbientLight(0xffffff, 0.03);
  }
  createSun() {
    this.sun = new THREE.SpotLight(0xffffff); // 0.1
    this.sun.intensity = 0.4;
    this.sun.distance = 100;
    this.sun.angle = Math.PI;
    this.sun.penumbra = 2.0;
    this.sun.decay = 1.0;
    this.sun.position.set(0, 50, 0);
  }
}
class World {
  constructor(opts) {
    this.opts = Object.assign({}, World.defaultOpts, opts);
    this.init();
  }
  init() {
    this.initScene();
    this.initCamera();
    this.initRenderer();
    this.addRenderer();
    window.addEventListener("resize", this.resizeHandler.bind(this));
  }
  initScene() {
    this.scene = new THREE.Scene();
  }
  initCamera() {
    this.camera = new THREE.PerspectiveCamera(
      this.opts.camFov,
      window.innerWidth / window.innerHeight,
      this.opts.camNear,
      this.opts.camFar
    );
    this.camera.position.set(
      this.opts.camPosition.x,
      this.opts.camPosition.y,
      this.opts.camPosition.z
    );
    this.camera.lookAt(this.scene.position);
    this.scene.add(this.camera);
  }
  initRenderer() {
    this.renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: true,
      logarithmicDepthBuffer: true
    });
    this.renderer.setSize(window.innerWidth, window.innerHeight);

    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  }
  addRenderer() {
    this.opts.container.appendChild(this.renderer.domElement);
  }
  resizeHandler() {
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.camera.aspect = window.innerWidth / window.innerHeight;
    this.camera.updateProjectionMatrix();
  }
  sceneAdd(obj) {
    this.scene.add(obj);
  }
}
World.defaultOpts = {
  container: document.body,
  camPosition: new THREE.Vector3(150, 200, 400),
  camFov: 6,
  camNear: 0.1,
  camFar: 800
};
class Composition {
  constructor(opts) {
    this.opts = Object.assign({}, Composition.defaultOpts, opts);
    this.tube = Tube({
      amount: this.opts.amount,
      radius: this.opts.radius,
      thickness: this.opts.thickness
    });
    this.tubes = this.createTubes();
  }
  createRow() {
    const radius = this.opts.radius + this.opts.offset;
    const geometry = new THREE.Geometry();
    for (let i = 0; i < this.opts.sideLength; i++) {
      const t = this.tube.clone();
      t.translate(i * radius * 2, 0, 0);
      geometry.merge(t);
    }
    return geometry;
  }
  createTubes() {
    const row = this.createRow();
    const radius = this.opts.radius + this.opts.offset;
    const geometry = new THREE.Geometry();

    for (let i = 0; i < this.opts.sideLength; i++) {
      const r = row.clone();
      r.translate(0, 0, i * radius * 2);
      geometry.merge(r);
    }
    geometry.center();
    const bufferGeometry = new THREE.BufferGeometry().fromGeometry(geometry);
    const materials = [
      new THREE.MeshStandardMaterial({
        color: 0x333333,
        roughness: 1.0,
        metalness: 0.0,
        emissive: 0x000000,
        flatShading: true,
        side: THREE.DoubleSide
      }),
      new THREE.MeshStandardMaterial({
        color: 0x333333,
        roughness: 0.6,
        metalness: 0.0,
        emissive: 0x000000,
        flatShading: true,
        side: THREE.DoubleSide
      })
    ];
    const mesh = new THREE.Mesh(bufferGeometry, materials);
    return mesh;
  }
}
Composition.defaultOpts = {
  sideLength: 10,
  amount: 15,
  radius: 6,
  thickness: 2,
  offset: 0.3
};
function createShape({ innerRadius = 4, outerRadius = 6, fineness = 30 }) {
  const outer = getPath(outerRadius, fineness, false);
  const baseShape = new THREE.Shape(outer);
  const inner = getPath(innerRadius, fineness, true);
  const baseHole = new THREE.Path(inner);
  baseShape.holes.push(baseHole);
  return baseShape;
}
const getPath = (radius, fineness, reverse) => {
  const c = radius * 0.55191502449;
  const path = new THREE.CurvePath();
  path.curves = [
    new THREE.CubicBezierCurve(
      new THREE.Vector2(0, radius),
      new THREE.Vector2(c, radius),
      new THREE.Vector2(radius, c),
      new THREE.Vector2(radius, 0)
    ),
    new THREE.CubicBezierCurve(
      new THREE.Vector2(radius, 0),
      new THREE.Vector2(radius, -c),
      new THREE.Vector2(c, -radius),
      new THREE.Vector2(0, -radius)
    ),
    new THREE.CubicBezierCurve(
      new THREE.Vector2(0, -radius),
      new THREE.Vector2(-c, -radius),
      new THREE.Vector2(-radius, -c),
      new THREE.Vector2(-radius, 0)
    ),
    new THREE.CubicBezierCurve(
      new THREE.Vector2(-radius, 0),
      new THREE.Vector2(-radius, c),
      new THREE.Vector2(-c, radius),
      new THREE.Vector2(0, radius)
    )
  ];
  const points = path.getPoints(fineness);
  if (reverse) points.reverse();
  return points;
};
function Tube({ amount = 4, radius = 6, thickness = 2 }) {
  const shape = createShape({
    innerRadius: radius - thickness,
    outerRadius: radius,
    fineness: 14
  });
  const props = {
    amount: amount,
    bevelEnabled: true,
    bevelThickness: 0.3,
    bevelSize: 0.2,
    bevelSegments: 1
  };
  const geometry = new THREE.ExtrudeGeometry(shape, props);
  geometry.center();
  geometry.computeVertexNormals();
  for (var i = 0; i < geometry.faces.length; i++) {
    var face = geometry.faces[i];
    if (face.materialIndex == 1) {
      for (var j = 0; j < face.vertexNormals.length; j++) {
        face.vertexNormals[j].z = 0;
        face.vertexNormals[j].normalize();
      }
    }
  }
  geometry.rotateX(Math.PI * 0.5);
  geometry.rotateZ(Math.PI);
  return geometry;
}
const app = new App();
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/three.js/89/three.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js