Загрузка...

Футуристичный 3D-карусель фон на Three.js. Интерактивный галерея-бэкграунд для сайтов с киберпанк-стилем. Идеально для лендингов.
<div id="spatial-root" class="relative w-full h-screen overflow-hidden bg-black text-[#EAEAEA]"
style="font-family: 'SF Mono', monospace; cursor: none; --c-grid: rgba(255, 255, 255, 0.08); --c-dim: #555555;">
<style>
.gl-line {
position: absolute;
background: var(--c-grid);
}
.crosshair::before,
.crosshair::after {
content: '';
position: absolute;
background: #EAEAEA;
}
.crosshair::before {
width: 100%;
height: 1px;
top: 50%;
left: 0;
}
.crosshair::after {
width: 1px;
height: 100%;
left: 50%;
top: 0;
}
.meta-text {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.15em;
}
.side-text {
writing-mode: vertical-rl;
}
</style>
<div id="loader"
class="fixed inset-0 z-[100] flex items-center justify-center bg-black transition-opacity duration-1000 text-[11px] tracking-[0.3em]">
SYSTEM_CALIBRATION...
</div>
<div id="cursor-ring"
class="fixed top-0 left-0 w-10 h-10 border border-white/40 rounded-full pointer-events-none z-[9999] mix-blend-difference -translate-x-1/2 -translate-y-1/2 transition-[width,height] duration-300">
</div>
<div id="cursor-dot"
class="fixed top-0 left-0 w-1 h-1 bg-white rounded-full pointer-events-none z-[9999] -translate-x-1/2 -translate-y-1/2">
</div>
<div class="absolute inset-0 z-10 grid grid-cols-[60px_1fr_60px] grid-rows-[60px_1fr_60px] pointer-events-none">
<div class="gl-line w-full h-px left-0 top-[60px]"></div>
<div class="gl-line w-full h-px left-0 bottom-[60px]"></div>
<div class="gl-line h-full w-px top-0 left-[60px]"></div>
<div class="gl-line h-full w-px top-0 right-[60px]"></div>
<div class="crosshair absolute w-2.5 h-2.5 top-[55px] left-[55px]"></div>
<div class="crosshair absolute w-2.5 h-2.5 top-[55px] right-[55px]"></div>
<div class="crosshair absolute w-2.5 h-2.5 bottom-[55px] left-[55px]"></div>
<div class="crosshair absolute w-2.5 h-2.5 bottom-[55px] right-[55px]"></div>
<header class="col-start-2 row-start-1 flex justify-between items-center px-5 meta-text">
<div class="flex items-center">
<span class="font-bold mr-4">CORE_SYSTEM</span>
<span class="text-[#555555]">NODE.V2.X</span>
</div>
<div>KINETIC / SYNERGY / FLOW</div>
<div id="time-display" class="text-[#555555]">00:00:00 [GMT]</div>
</header>
<div class="col-start-1 row-start-2 flex justify-center items-center side-text rotate-180 meta-text">
<span class="text-[#555555]">DATA_STREAM [ENCRYPTED]</span>
</div>
<div class="col-start-3 row-start-2 flex justify-center items-center side-text meta-text">
<span>LATENCY_SYNC</span>
</div>
<footer class="col-start-2 row-start-3 flex justify-between items-center px-5 meta-text">
<div class="flex items-center">
<div class="w-1.5 h-1.5 bg-[#00FF41] rounded-full mr-3 shadow-[0_0_12px_#00FF41]"></div>
<span>RESONANCE STABLE</span>
</div>
<div id="slide-counter" class="text-[#555555]">01 / 05</div>
<div class="w-[100px]"></div>
</footer>
<div class="absolute bottom-0 left-0 w-full h-[2px] bg-white/10">
<div id="progress-bar" class="h-full bg-white w-0 transition-[width] duration-100 ease-out"></div>
</div>
</div>
<div id="gl-container" class="fixed inset-0 z-0"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<script>
(function() {
const config = { speed: 1.8, damping: 0.08, cardW: 3.8, cardH: 2.2, gap: 4.8 };
const content = [
{ img: 'https://images.unsplash.com/photo-1614850523296-d8c1af93d400?q=80&w=2000&auto=format&fit=crop', title: 'QUANTUM_SHIFT' },
{ img: 'https://images.unsplash.com/photo-1635776062127-d379bfcbb9c8?q=80&w=2000&auto=format&fit=crop', title: 'PLASMA_ARC' },
{ img: 'https://images.unsplash.com/photo-1614728263952-84ea256f9679?q=80&w=2000&auto=format&fit=crop', title: 'CYBER_SPHERE' },
{ img: 'https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?q=80&w=2000&auto=format&fit=crop', title: 'DARK_MATTER' },
{ img: 'https://images.unsplash.com/photo-1620641788421-7a1c342ea42e?q=80&w=2000&auto=format&fit=crop', title: 'LIGHT_SYNC' }
];
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050505, 0.12);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.z = 6;
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('gl-container').appendChild(renderer.domElement);
const vsh = `varying vec2 vUv; uniform float uTime; uniform float uSpeed; uniform float uOffset;
void main() { vUv = uv; vec3 pos = position;
float wave = sin(pos.x * 1.5 + uTime * 2.0 + uOffset * 2.0) * cos(pos.y * 1.5 + uTime * 1.5);
pos.z += wave * 0.25 + sin(pos.x * 2.0) * uSpeed * 0.3;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); }`;
const fsh = `uniform sampler2D uTexture; uniform float uTime; uniform float uHover; varying vec2 vUv;
float rnd(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); }
void main() { vec2 uv = vUv; float ripple = sin(uv.x * 10.0 + uTime * 2.0) * 0.005 * uHover;
vec4 tex = texture2D(uTexture, uv + ripple); float gray = dot(tex.rgb, vec3(0.299, 0.587, 0.114));
vec3 col = mix(vec3(gray), tex.rgb, 0.3 + (uHover * 0.7)) + rnd(uv * uTime) * 0.12;
col *= smoothstep(0.9, 0.2, distance(uv, vec2(0.5)));
gl_FragColor = vec4(col - sin(uv.y * 600.0 + uTime * 10.0) * 0.02, 1.0); }`;
const meshes = [], group = new THREE.Group(); scene.add(group);
const texLoader = new THREE.TextureLoader();
content.forEach((item, i) => {
const mat = new THREE.ShaderMaterial({
uniforms: { uTexture: { value: texLoader.load(item.img) }, uTime: { value: 0 }, uSpeed: { value: 0 }, uOffset: { value: i * config.gap }, uHover: { value: 0 } },
vertexShader: vsh, fragmentShader: fsh, transparent: true, side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(config.cardW, config.cardH, 64, 64), mat);
mesh.position.x = i * config.gap; mesh.userData = { index: i, originalX: i * config.gap };
group.add(mesh); meshes.push(mesh);
});
let scroll = 0, scrollT = 0, mouse = new THREE.Vector2(), ray = new THREE.Raycaster();
window.addEventListener('wheel', e => scrollT += e.deltaY * 0.002 * config.speed);
window.addEventListener('mousemove', e => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1; mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
gsap.to('#cursor-ring', { x: e.clientX, y: e.clientY, duration: 0.15 });
gsap.to('#cursor-dot', { x: e.clientX, y: e.clientY, duration: 0.05 });
});
setInterval(() => { document.getElementById('time-display').innerText = new Date().toISOString().split('T')[1].split('.')[0] + " [GMT]"; }, 1000);
function animate() {
const time = performance.now() * 0.001;
scroll += (scrollT - scroll) * config.damping;
const totalW = content.length * config.gap;
let activeIdx = 0, minDist = Infinity;
meshes.forEach(m => {
let x = m.userData.originalX - scroll;
while (x < -totalW / 2) x += totalW; while (x > totalW / 2) x -= totalW;
m.position.x = x;
const d = Math.abs(x);
m.position.y = Math.sin(x * 0.8 + time) * 0.45;
m.position.z = Math.cos(x * 0.8 + time) * 0.45 - Math.pow(d * 0.35, 2);
m.rotation.y = -x * 0.08; m.material.uniforms.uTime.value = time;
m.material.uniforms.uSpeed.value = scrollT - scroll;
m.material.uniforms.uOffset.value = x;
if (d < minDist) { minDist = d; activeIdx = m.userData.index; }
});
ray.setFromCamera(mouse, camera);
const hits = ray.intersectObjects(meshes);
meshes.forEach(m => gsap.to(m.material.uniforms.uHover, { value: 0, duration: 0.6 }));
if (hits.length > 0) {
gsap.to(hits[0].object.material.uniforms.uHover, { value: 1, duration: 0.4 });
gsap.to('#cursor-ring', { width: 64, height: 64, duration: 0.3 });
} else { gsap.to('#cursor-ring', { width: 40, height: 40, duration: 0.3 }); }
document.getElementById('slide-counter').innerText = `0${activeIdx + 1} / 0${content.length}`;
document.getElementById('progress-bar').style.width = `${(activeIdx / (content.length - 1)) * 100}%`;
renderer.render(scene, camera); requestAnimationFrame(animate);
}
window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });
animate();
setTimeout(() => { gsap.to('#loader', { opacity: 0, duration: 1, onComplete: () => document.getElementById('loader').style.display = 'none' }); }, 1500);
})();
</script>
</div>