<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mirrored Rings Animation</title>
</head>
<body style="margin: 0; padding: 0; background-color: #000;">
<mirrored-rings-animation style="width: 1000px; height: 1000px;"></mirrored-rings-animation>
<script type="module">
class MirroredRingsAnimation extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Define the styles for the component's container and canvas
const style = document.createElement('style');
style.textContent = `
:host {
display: block;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
width: 100%;
height: 100%;
background-color: #000;
}
`;
this.shadowRoot.appendChild(style);
// Load the three.js library dynamically. The animation will start once it's loaded.
const threeScript = document.createElement('script');
threeScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
threeScript.onload = () => this.initAnimation();
this.shadowRoot.appendChild(threeScript);
}
initAnimation() {
// --- SCENE SETUP ---
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, this.clientWidth / this.clientHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(this.clientWidth, this.clientHeight);
this.shadowRoot.appendChild(renderer.domElement); // Append canvas to the component, not the page body
// Create a group for the camera to control its orbit and tilt
const cameraGroup = new THREE.Group();
scene.add(cameraGroup);
cameraGroup.add(camera);
// Position the camera away from the center of the group
camera.position.z = 15;
// Tilt the camera group by 15 degrees on the x-axis for an angled view
cameraGroup.rotation.x = 15 * (Math.PI / 180);
// --- LIGHTING ---
const ambientLight = new THREE.AmbientLight(0x404040, 5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
directionalLight.position.set(0, 1, 1).normalize();
scene.add(directionalLight);
// --- PARAMETERS ---
const colors = [0xff5733, 0xff3333, 0x5733ff, 0x33e6];
const majorRadius = 5.35;
const rungCount = 107;
const totalRotation = Math.PI * 2;
const dumbbells1 = [];
const dumbbells2 = [];
let time = 0;
const swirlSpeed = 0.01;
const individualRotationSpeed = 6;
const cameraOrbitSpeed = 0.005; // New variable for camera's rotation speed
const spacingOffset = 1.5; // New variable for horizontal spacing
// --- DUMBBELL CREATION FUNCTION ---
function createDumbbell(color) {
const dumbbellGroup = new THREE.Group();
const ballGeometry = new THREE.SphereGeometry(0.2, 16, 16);
const ballMaterial = new THREE.MeshPhongMaterial({ color: color });
const dot1 = new THREE.Mesh(ballGeometry, ballMaterial);
const dot2 = new THREE.Mesh(ballGeometry, ballMaterial);
const barLength = 1;
const rungGeometry = new THREE.CylinderGeometry(0.1, 0.1, barLength, 8);
const rungMaterial = new THREE.MeshPhongMaterial({ color: color });
const rung = new THREE.Mesh(rungGeometry, rungMaterial);
dot1.position.y = barLength / 2;
dot2.position.y = -barLength / 2;
dumbbellGroup.add(dot1, dot2, rung);
return dumbbellGroup;
}
// --- OBJECT INITIALIZATION ---
for (let i = 0; i < rungCount; i++) {
const randomColor = colors[Math.floor(Math.random() * colors.length)];
const dumbbell1 = createDumbbell(randomColor);
const dumbbell2 = createDumbbell(randomColor);
dumbbells1.push(dumbbell1);
dumbbells2.push(dumbbell2);
scene.add(dumbbell1, dumbbell2);
}
// --- ANIMATION LOOP ---
const animate = () => {
requestAnimationFrame(animate);
time += swirlSpeed;
// Rotate the camera group to create the circling effect
cameraGroup.rotation.y += cameraOrbitSpeed;
for (let i = 0; i < rungCount; i++) {
const t = (i / rungCount) * totalRotation;
const animatedTime = t + time;
// Ring 1 (XY Plane) - shifted to the right
const x1 = majorRadius * Math.cos(animatedTime) + spacingOffset;
const y1 = majorRadius * Math.sin(animatedTime);
const pos1 = new THREE.Vector3(x1, y1, 0);
const pos2 = new THREE.Vector3(x1, -y1, 0);
const dumbbell1 = dumbbells1[i];
dumbbell1.position.copy(pos1);
const alignmentQuaternion1 = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), pos2.clone().sub(pos1).normalize());
const initialXAngle1 = i * (15 * (Math.PI / 180));
const continuousRotation1 = time * individualRotationSpeed;
const totalXRotation1 = initialXAngle1 + continuousRotation1;
const localRotationQuaternion1 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), totalXRotation1);
dumbbell1.quaternion.copy(alignmentQuaternion1).multiply(localRotationQuaternion1);
// Ring 2 (XZ Plane) - shifted to the left
const x2_ring = majorRadius * Math.cos(animatedTime) - spacingOffset;
const z2_ring = majorRadius * Math.sin(animatedTime);
const pos1_ring2 = new THREE.Vector3(x2_ring, 0, z2_ring);
const pos2_ring2 = new THREE.Vector3(x2_ring, 0, -z2_ring);
const dumbbell2 = dumbbells2[i];
dumbbell2.position.copy(pos1_ring2);
const alignmentQuaternion2 = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), pos2_ring2.clone().sub(pos1_ring2).normalize());
const initialXAngle2 = i * (15 * (Math.PI / 180));
const continuousRotation2 = time * individualRotationSpeed;
const totalXRotation2 = initialXAngle2 + continuousRotation2;
const localRotationQuaternion2 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), totalXRotation2);
dumbbell2.quaternion.copy(alignmentQuaternion2).multiply(localRotationQuaternion2);
}
renderer.render(scene, camera);
};
animate();
// --- RESIZE HANDLING ---
// This makes the animation responsive to the element's container size.
const resizeObserver = new ResizeObserver(entries => {
const entry = entries[0];
const { width, height } = entry.contentRect;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
});
resizeObserver.observe(this);
}
}
// Register the custom element with the browser
customElements.define('mirrored-rings-animation', MirroredRingsAnimation);
</script>
</body>
</html>