<canvas id="myCanvas"></canvas>
html {
  font-size: 0.655vw !important;
}

canvas {
  width: 60rem !important;
  height: 60rem !important;
  aspect-ratio: 1 / 1;
}
let scene, camera, renderer;
let path, tubeGeometry;
const molecules = [];
const moleculeCount = 2000;
const moleculeGeometry = new THREE.SphereGeometry(0.05, 16, 16);
const moleculeMaterial = new THREE.MeshBasicMaterial({ color: 0x0000ff });
const canvas = document.getElementById("myCanvas");
const tubeRadius = canvas.offsetWidth / 176;
const segmentCount = 100;
const rotationAngle = 5 * Math.PI / 6; // 45 degrees in radians
const speedCoefficient = 0.0001; // Coefficient for speed adjustment (slower)
let clock = new THREE.Clock();
const fps = 25; // Frames per second
let interval = 1000 / fps;
let lastTime = 0;
let isAnimating = true; // Flag to control animation

const pointOnPath = new THREE.Vector3();
const randomOffset = new THREE.Vector3();

function init() {
    // Set up scene, camera, and renderer
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000000); // Set background color to black
    camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
    renderer = new THREE.WebGLRenderer({ canvas: canvas });
    renderer.setSize(600, 600);

    createOvalPath();
    createMolecules();
    updateCameraPosition();

    window.addEventListener("resize", onWindowResize, false);
    document.addEventListener("visibilitychange", onVisibilityChange, false); // Listen for visibility changes
    onWindowResize();

    animate(0);
}

function createOvalPath() {
    // Create an oval-shaped tube geometry proportional to canvas size
    const canvasSize = Math.min(renderer.domElement.width, renderer.domElement.height);
    const a = canvasSize / 37; // semi-major axis (width)
    const b = canvasSize / 73; // semi-minor axis (height)
    const points = [];

    for (let i = 0; i <= segmentCount; i++) {
        const angle = (i / segmentCount) * Math.PI * 2;
        const x = a * Math.cos(angle);
        const y = b * Math.sin(angle);
        const z = 0;

        // Rotate the point around Y axis by 45 degrees
        const rotatedX = x * Math.cos(rotationAngle) - z * Math.sin(rotationAngle);
        const rotatedZ = x * Math.sin(rotationAngle) + z * Math.cos(rotationAngle);

        points.push(new THREE.Vector3(rotatedX - canvasSize / 80, y, rotatedZ));
    }

    path = new THREE.CatmullRomCurve3(points);
    tubeGeometry = new THREE.TubeGeometry(path, 200, tubeRadius, 8, true);
}

function createMolecules() {
    molecules.length = 0; // Clear existing molecules

    for (let i = 0; i < moleculeCount; i++) {
        const molecule = new THREE.Mesh(moleculeGeometry, moleculeMaterial);
        scene.add(molecule);
        molecules.push({
            mesh: molecule,
            position: Math.random(),
            speed: Math.random() * 0.01 + 0.00005 // Slower speed
        });
    }
}

function updateCameraPosition() {
    const canvasSize = Math.min(renderer.domElement.width, renderer.domElement.height);
    camera.position.z = canvasSize / 28.5;
}

function onWindowResize() {
    const canvasSize = Math.min(window.innerWidth, window.innerHeight);
    renderer.setSize(canvasSize, canvasSize);
    camera.aspect = 1;
    camera.updateProjectionMatrix();

    createOvalPath(); // Recreate oval path based on new canvas size
    updateCameraPosition();
}

function onVisibilityChange() {
    isAnimating = !document.hidden; // Pause animation if page is not visible
    if (isAnimating) {
        animate(lastTime);
    }
}

function animate(time) {
    if (!isAnimating) return; // Do not animate if the flag is set to false

    requestAnimationFrame(animate);

    const delta = clock.getDelta(); // Get time elapsed since last frame

    if (time - lastTime >= interval) {
        lastTime = time;

        molecules.forEach(molecule => {
            molecule.position += molecule.speed * delta * 15;
            if (molecule.position > 1) molecule.position -= 1; // Keep within the range [0, 1]
            
            path.getPointAt(molecule.position, pointOnPath);
            randomOffset.set(
                (Math.random() - 0.5) * tubeRadius,
                (Math.random() - 0.5) * tubeRadius,
                (Math.random() - 0.5) * tubeRadius
            );

            molecule.mesh.position.lerp(pointOnPath, 0.05);
        });

        renderer.render(scene, camera);
    }
}

init();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js