Bu çalışma, HTML, CSS ve saf JavaScript kullanarak oluşturulmuş, kullanıcı etkileşimini merkeze alan 3D bir görsel sergileme deneyimidir. Projenin temelinde yatan matematiksel hesaplamalar, görsellerin bir yörünge üzerinde akıcı bir şekilde dönmesini ve derinlik algısının kusursuz yansıtılmasını sağlar.
Kullanıcılar, fare veya dokunmatik ekran üzerinden sürükle-bırak (drag and spin) mekanizmasıyla galeriyi istedikleri yöne doğru hareket ettirebilirler. Gelişmiş ivme algoritması sayesinde, sürükleme işlemi bırakıldığında galeri yumuşak bir sürtünme efektiyle yavaşlayarak durur.
Görsel hiyerarşiyi güçlendirmek adına, en öne gelen resim devasa bir boyuta ulaşarak odak noktası haline gelirken, arkada kalan resimler otomatik olarak küçülür ve soluklaşır. Bu dinamik yapıya ek olarak, arka plan katmanı en öndeki resme göre %50 saydamlık ve bulanıklık efektiyle anlık olarak güncellenerek sinematik bir atmosfer oluşturur.
Tamamen performans odaklı kod yapısı ile hazırlanan bu galeri, hem masaüstü hem de mobil cihazlarda akıcı bir kullanıcı deneyimi sunmaktadır.
<!doctype html>
<html lang="tr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D Odaklı Galeri - Arka Plan Efektli</title>
<style>
body {
background-color: #000;
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
font-family: sans-serif;
user-select: none;
}
#bg-layer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
opacity: 0.8;
filter: blur(5px);
transition: background-image 0.8s ease-in-out;
z-index: -1; /* En arkada kalması için */
}
/* Karartma katmanı (resmin çok parlak olmaması için) */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle, transparent 20%, #000 100%);
z-index: 0;
}
.stage {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
perspective: 2000px;
cursor: grab;
z-index: 10;
}
.stage:active {
cursor: grabbing;
}
.carousel-item {
position: absolute;
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
pointer-events: none;
transition:
transform 0.2s ease-out,
opacity 0.2s ease-out,
box-shadow 0.2s ease-out;
will-change: transform, opacity;
}
.carousel-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
</head>
<body>
<div id="bg-layer"></div>
<div class="overlay"></div>
<div class="stage" id="stage">
<div class="carousel-item">
<img src="https://picsum.photos/600?random=61" />
</div>
<div class="carousel-item">
<img src="https://picsum.photos/600?random=62" />
</div>
<div class="carousel-item">
<img src="https://picsum.photos/600?random=63" />
</div>
<div class="carousel-item">
<img src="https://picsum.photos/600?random=64" />
</div>
<div class="carousel-item">
<img src="https://picsum.photos/600?random=65" />
</div>
<div class="carousel-item">
<img src="https://picsum.photos/600?random=66" />
</div>
<div class="carousel-item">
<img src="https://picsum.photos/600?random=67" />
</div>
<div class="carousel-item">
<img src="https://picsum.photos/600?random=68" />
</div>
<div class="carousel-item">
<img src="https://picsum.photos/600?random=69" />
</div>
</div>
<script>
const items = document.querySelectorAll(".carousel-item");
const bgLayer = document.getElementById("bg-layer");
const numItems = items.length;
let isDragging = false;
let startX = 0;
let currentAngle = 0;
let targetAngle = 0;
let velocity = 0;
let lastFrontIndex = -1;
const radiusX = 450;
const radiusY = 50;
const sensitivity = 0.005;
const friction = 0.95;
const startDrag = (e) => {
isDragging = true;
startX = e.pageX || (e.touches && e.touches[0].pageX);
velocity = 0;
};
const onDrag = (e) => {
if (!isDragging) return;
if (e.touches) e.preventDefault();
const x = e.pageX || e.touches[0].pageX;
const diff = (startX - x) * sensitivity;
targetAngle += diff;
startX = x;
velocity = diff;
};
const stopDrag = () => (isDragging = false);
window.addEventListener("mousedown", startDrag);
window.addEventListener("mousemove", onDrag);
window.addEventListener("mouseup", stopDrag);
window.addEventListener("touchstart", startDrag, { passive: false });
window.addEventListener("touchmove", onDrag, { passive: false });
window.addEventListener("touchend", stopDrag);
function animate() {
if (!isDragging) {
targetAngle += velocity;
velocity *= friction;
}
currentAngle += (targetAngle - currentAngle) * 0.1;
let maxDepth = -Infinity;
let currentFrontImg = "";
let currentFrontIndex = -1;
items.forEach((item, index) => {
const itemAngle = currentAngle + (index * (Math.PI * 2)) / numItems;
const x = Math.cos(itemAngle) * radiusX;
const y = Math.sin(itemAngle) * radiusY;
const depth = Math.sin(itemAngle);
const normalizedDepth = (depth + 1) / 2;
const scale = 0.4 + Math.pow(normalizedDepth, 8) * 3.0;
const opacity = 0.1 + Math.pow(normalizedDepth, 4) * 0.9;
const zIndex = Math.floor(normalizedDepth * 1000);
if (depth > maxDepth) {
maxDepth = depth;
currentFrontImg = item.querySelector("img").src;
currentFrontIndex = index;
}
item.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%) scale(${scale})`;
item.style.opacity = opacity;
item.style.zIndex = zIndex;
if (depth > 0.9) {
item.style.boxShadow = `0 40px 100px rgba(0,0,0,1), 0 0 40px rgba(255,255,255,0.2)`;
} else {
item.style.boxShadow = `none`;
}
});
if (currentFrontIndex !== lastFrontIndex) {
bgLayer.style.backgroundImage = `url('${currentFrontImg}')`;
lastFrontIndex = currentFrontIndex;
}
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>