<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