Gallery Navigation
Пример создания галереи с изображениями, красивыми заголовками, параллаксом и миниатюрной навигацией
JSON
Данные в формате JSON
[
{
"title": "DUBLDOM",
"link": "https://test.ru",
"image": "./assets/images/1.JPG"
},
{
"title": "JUNIPER TREE",
"link": "https://test.ru",
"image": "./assets/images/2.JPG"
},
{
"title": "Chill out",
"link": "https://test.ru",
"image": "./assets/images/3.JPG"
},
{
"title": "Kandalaksha",
"link": "https://test.ru",
"image": "./assets/images/4.JPG"
}
]
PHP
Базовая разметка
$json = file_get_contents(__DIR__ . '/assets/projects.json');
$projects = json_decode( $json, true );
foreach ( $projects as $project ) {
?>
<div
class="projects__item"
data-gallery-item
data-image="<?= $project['image'] ?>"
>
<div class="projects__item-bg">
<img src="<?= $project['image'] ?>" alt="">
</div>
<a
href="<?= $project['link'] ?>"
target="_blank"
rel="noopener noreferrer"
title="<?= $project['title'] ?>"
class="projects__item-link"
>
<?= $project['title'] ?>
</a>
</div>
<?php
}
JavaScript
Вся логика в одном классе
class Gallery {
/**
* @type {HTMLElement}
*/
container;
items = [];
/**
* @type {DOMRect}
*/
rect;
/**
* @type {HTMLElement}
*/
navigation;
interpolatedProgress = 0;
isInViewPort = false;
isInViewPortFull = false;
/**
*
* @param {HTMLElement} container
* @returns
*/
constructor(container) {
if (!container) {
return
}
this.container = container;
this.fillItems();
this.buildNavigation();
this.loop();
}
update() {
this.handleNavigation();
this.handleItemParallax();
this.handleTitles();
}
handleNavigation() {
if (this.isInViewPortFull) {
this.navigation.classList.add('active');
} else {
this.navigation.classList.remove('active');
}
this.interpolatedProgress = -this.interpolation(this.rect.top, 0, this.rect.height
- window.innerHeight, 0, 100 * (this.items.length - 1));
if (this.interpolatedProgress < 0) {
this.interpolatedProgress = 0;
}
if (this.interpolatedProgress > this.rect.height - window.innerHeight) {
this.interpolatedProgress = this.rect.height - window.innerHeight;
}
const frame = this.navigation.querySelector('.projects__nav-frame');
frame.style.transform = `translate3d(${this.interpolatedProgress}%, 0, 0)`;
}
handleItemParallax() {
this.items.forEach(item => {
if (!item.isInViewPort) {
return;
}
item.progress = this.interpolation(item.rect.top, 0, item.rect.height, 0, -50);
const image = item.el.querySelector('img');
image.style.transform = `translate3d(0, ${item.progress}%, 0)`;
});
}
handleTitles() {
this.items.forEach(item => {
if (!item.prepared) {
this.prepareTitle(item);
return
}
if (!item.isInViewPort) {
return;
}
const text = item.el.querySelector('.projects__item-link');
text.style.transform = `translate3d(0, ${item.progress * 5}px, 0)`;
if (Math.abs(item.progress) >= 20) {
this.hideTitle(text);
} else {
this.revealTitle(text);
}
});
}
getRect() {
this.rect = this.container.getBoundingClientRect();
}
checkViewPort() {
this.isInViewPort = this.rect.top < window.innerHeight
&& this.rect.top + this.rect.height > 0;
this.isInViewPortFull = this.rect.top <= 1
&& this.rect.top + this.rect.height >= window.innerHeight;
if (!this.isInViewPort) {
return;
}
this.items.forEach(item => {
item.rect = item.el.getBoundingClientRect();
item.isInViewPort = item.rect.top <= window.innerHeight
&& item.rect.top + item.rect.height >= 0;
});
}
loop() {
this.getRect();
this.checkViewPort();
if (this.isInViewPort) {
this.update();
}
requestAnimationFrame(this.loop.bind(this));
}
fillItems() {
const items = this.container.querySelectorAll('[data-gallery-item]');
items.forEach((el, ind) => {
this.items.push({
index: ind,
el: el,
isInViewPort: false,
rect: null,
progress: 0,
title: el.querySelector('.projects__item-link')?.textContent,
});
});
}
buildNavigation() {
const navEl = document.createElement('div');
navEl.classList.add('projects__nav');
navEl.innerHTML = `
<div class="projects__nav-frame"></div>
`;
this.items.forEach(item => {
const el = document.createElement('div');
el.classList.add('projects__nav-item');
el.dataset.index = item.index;
el.title = item.title;
el.innerHTML = `<img src="${item.el.dataset.image}" alt="">`;
navEl.append(el);
el.addEventListener('click', (e) => {
e.preventDefault();
this.onNavItemClick(item);
});
});
this.navigation = navEl;
this.container.append(navEl);
}
onNavItemClick(item) {
item.el.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
prepareTitle(item) {
const text = item.el.querySelector('.projects__item-link');
if (!text) {
return;
}
item.prepared = true;
let spanCount = 0;
const words = text.textContent.trim().split(' ');
text.innerHTML = '';
words.forEach(word => {
const wordEl = document.createElement('span');
const letters = word.split('');
letters.forEach(letter => {
const span = document.createElement('span');
span.innerHTML = letter;
span.dataset.index = spanCount;
span.setAttribute('style', `--coeff:${spanCount}`);
wordEl.append(span);
spanCount += 1;
});
const span = document.createElement('span');
span.innerHTML = ' ';
wordEl.append(span);
text.append(wordEl);
});
}
hideTitle(el) {
el.classList.remove('reveal');
}
revealTitle(el) {
el.classList.add('reveal');
}
interpolation(value, min, max, newMin, newMax) {
let newValue = ((value - min) / (max - min)) * (newMax - newMin) + newMin;
return newValue;
}
};
const initGallery = () => {
Array.from(document.querySelectorAll('[data-gallery]')).forEach(el => {
new Gallery(el);
});
};
initGallery();
CSS
Какие-то стили, для красоты
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
font-family: "Montserrat", Arial, sans-serif;
font-size: 1.1vw;
line-height: 1.5;
}
p {
margin: 2rem 0;
}
.container {
padding: 1rem 3rem;
}
.projects__item {
display: flex;
justify-content: center;
align-items: center;
}
.projects__item-bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.projects__item-bg img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
filter: brightness(50%);
}
.projects__item-link {
position: relative;
z-index: 1;
text-align: center;
text-decoration: none;
font-size: 4rem;
color: white;
font-weight: 900;
text-transform: uppercase;
}
.projects__nav {
position: fixed;
left: 50%;
bottom: 1rem;
width: max-content;
z-index: 2;
display: flex;
justify-content: center;
transition: 0.25s ease-in-out;
visibility: hidden;
pointer-events: none;
opacity: 0;
transform: translateX(-50%);
}
.projects__nav::before {
content: "";
position: absolute;
left: 50%;
bottom: -1rem;
width: 100vw;
height: calc(100% + 4rem);
transform: translate(-50%, 0);
z-index: -1;
pointer-events: none;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
mix-blend-mode: overlay;
}
.projects__nav.active {
visibility: visible;
pointer-events: auto;
opacity: 1;
}
.projects__nav-frame {
width: 7rem;
height: 4rem;
border: 2px solid #61cced;
position: absolute;
left: 0;
top: 0;
pointer-events: none;
background-color: rgba(0, 0, 0, 0.3);
}
.projects__nav-item {
width: 7rem;
height: 4rem;
cursor: pointer;
}
.projects__nav-item img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.projects__item-link span[data-index] {
opacity: 0;
display: inline-block;
will-change: transform;
transform: translate3d(0.5rem, 0.5rem, 0);
transition: 0.5s ease-in-out;
transition-delay: calc(0.03s * var(--coeff));
}
.projects__item-link.reveal span[data-index] {
opacity: 1;
transform: translate3d(0, 0, 0);
}
[data-gallery] {
position: relative;
}
[data-gallery-item] {
position: relative;
height: 100vh;
overflow: hidden;
}