Шаблон на основе сетки Masonry с возможностью скролла
В сегодняшнем уроке я бы хотел поделиться с вами шаблоном на основе сетки Masonry, который имеет прикольную анимацию при на ведении, а при открытии контент заполняет всю область экрана с возможностью прокрутки. После того как сделан клик по элементу, изображение плавно перемещается в центр страницы и подстраивается под размеры. Задний фон также масштабируется, заполняя всю область страницы. Отображаемый контент замечательно прокручивается и может вместить неограниченное количество текста. Анимация создана с использованием технологии TweenMax.
Обратите внимание: мы используем современные свойства CSS, которые могут не поддерживаться в старых браузерах.
HTML код я разбирать не буду. Его вы можете найти в этом файле в моем репозитории на gitHub: index.html
Перейдем непосредственно к самому сложному - это js файл, оригинал также можно найти здесь: demo.js
Определяем константы
// Вычисляет смещение сверху или слева от элемента относительно видового экрана
// (не считая преобразований, которые может иметь элемент)
const getOffset = (elem, axis) => {
let offset = 0;
const type = axis === 'top' ? 'offsetTop' : 'offsetLeft';
do {
if ( !isNaN( elem[type] ) )
{
offset += elem[type];
}
} while( elem = elem.offsetParent );
return offset;
}
// Вычисляем расстояние между двумя точками
const distance = (p1,p2) => Math.hypot(p2.x-p1.x, p2.y-p1.y);
// Получаем рандомное число
const randNumber = (min,max) => Math.floor(Math.random() * (max - min + 1)) + min;
// Получаем положение мыши. Взято от сюда: http://www.quirksmode.org/js/events_properties.html#position
const getMousePos = (e) => {
let posx = 0;
let posy = 0;
if (!e) e = window.event;
if (e.pageX || e.pageY) {
posx = e.pageX;
posy = e.pageY;
}
else if (e.clientX || e.clientY) {
posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
return { x : posx, y : posy }
};
// Возвращает угол поворота элемента.
const getAngle = (el) => {
const st = window.getComputedStyle(el, null);
const tr = st.getPropertyValue('transform');
let values = tr.split('(')[1];
values = values.split(')')[0];
values = values.split(',');
return Math.round(Math.asin(values[1]) * (180/Math.PI));
};
//Функции управления прокруткой. Взято из https://stackoverflow.com/a/4770179
const keys = {37: 1, 38: 1, 39: 1, 40: 1};
const preventDefault = (e) => {
e = e || window.event;
if (e.preventDefault)
e.preventDefault();
e.returnValue = false;
}
const preventDefaultForScrollKeys = (e) => {
if (keys[e.keyCode]) {
preventDefault(e);
return false;
}
}
const disableScroll = () => {
if (window.addEventListener) // для старых FF
window.addEventListener('DOMMouseScroll', preventDefault, false);
window.onwheel = preventDefault; // современный стандарт
window.onmousewheel = document.onmousewheel = preventDefault; // старые браузеры, IE
window.ontouchmove = preventDefault; // мобилка
document.onkeydown = preventDefaultForScrollKeys;
}
const enableScroll = () => {
if (window.removeEventListener)
window.removeEventListener('DOMMouseScroll', preventDefault, false);
window.onmousewheel = document.onmousewheel = null;
window.onwheel = null;
window.ontouchmove = null;
document.onkeydown = null;
}
Класс для работы с элементами сетки
class GridItem {
constructor(el) {
this.DOM = {el: el};
// Обертка для элемента вокруг изображения.
this.DOM.bg = this.DOM.el.querySelector('.grid__item-bg');
// Следующие элементы DOM-это элементы, которые будут перемещаться / наклоняться при наведении на элемент.
this.DOM.tilt = {};
// Работа с картинкой.
this.DOM.imgWrap = this.DOM.el.querySelector('.grid__item-wrap');
this.DOM.tilt.img = this.DOM.imgWrap.querySelector('img');
// Вертикальный заголовок
this.DOM.tilt.title = this.DOM.el.querySelector('.grid__item-title');
// Горизонтальный заголовок
this.DOM.tilt.number = this.DOM.el.querySelector('.grid__item-number');
// Разбиваем горизонтальный заголовок на много спанов, используем библиотетку charming.js
charming(this.DOM.tilt.number);
// И получаем наши "буквы"
this.DOM.numberLetters = this.DOM.tilt.number.querySelectorAll('span');
// Настройка при перемещении / наклоне элементов при наведении.
this.tiltconfig = {
title: {translation : {x: [-8,8], y: [4,-4]}},
number: {translation : {x: [-5,5], y: [-10,10]}},
img: {translation : {x: [-15,15], y: [-10,10]}}
};
// Получить значение угла поворота элемента изображения.
// Это будет использоваться для поворота модели DOM.bg к тому же значению при расширении / открытии элемента.
this.angle = getAngle(this.DOM.tilt.img);
// Запускаем функцию initEvents
this.initEvents();
}
initEvents() {
/**
* При наведении мыши:
* - Масштабирование модели DOM.bg элемент.
* - Анимация букв.
*
* Движение мыши:
* - наклон - движение как букв, картинки, так и заголовка
*
*
* Выход мыши за приделы обьекта:
* - Возврат масштаба Dom.bg элемента
* - Анимация букв.
*/
this.toggleAnimationOnHover = (type) => {
// Масштабирование bg элемента
TweenMax.to(this.DOM.bg, 1, {
ease: Expo.easeOut,
scale: type === 'mouseenter' ? 1.15 : 1
});
// Анимация букв
this.DOM.numberLetters.forEach((letter,pos) => {
TweenMax.to(letter, .2, {
ease: Quad.easeIn,
delay: pos*.1,
y: type === 'mouseenter' ? '-50%' : '50%',
opacity: 0,
onComplete: () => {
TweenMax.to(letter, type === 'mouseenter' ? 0.6 : 1, {
ease: type === 'mouseenter' ? Expo.easeOut : Elastic.easeOut.config(1,0.4),
startAt: {y: type === 'mouseenter' ? '70%' : '-70%', opacity: 0},
y: '0%',
opacity: 1
});
}
});
});
};
this.mouseenterFn = (ev) => {
if ( !allowTilt ) return;
this.toggleAnimationOnHover(ev.type);
};
this.mousemoveFn = (ev) => requestAnimationFrame(() => {
if ( !allowTilt ) return;
this.tilt(ev);
});
this.mouseleaveFn = (ev) => {
if ( !allowTilt ) return;
this.resetTilt();
this.toggleAnimationOnHover(ev.type);
};
this.DOM.el.addEventListener('mouseenter', this.mouseenterFn);
this.DOM.el.addEventListener('mousemove', this.mousemoveFn);
this.DOM.el.addEventListener('mouseleave', this.mouseleaveFn);
}
tilt(ev) {
// Получаю положение мыши
const mousepos = getMousePos(ev);
// Прокрутка документа.
const docScrolls = {left : body.scrollLeft + docEl.scrollLeft, top : body.scrollTop + docEl.scrollTop};
const bounds = this.DOM.el.getBoundingClientRect();
// Положение мыши относительно основного элемента (this.DOM.el).
const relmousepos = {
x : mousepos.x - bounds.left - docScrolls.left,
y : mousepos.y - bounds.top - docScrolls.top
};
// Настройки перемещения элементов наклона.
for (let key in this.DOM.tilt) {
let t = this.tiltconfig[key].translation;
// Анимируем каждый элемент
TweenMax.to(this.DOM.tilt[key], 2, {
ease: Expo.easeOut,
x: (t.x[1]-t.x[0])/bounds.width*relmousepos.x + t.x[0],
y: (t.y[1]-t.y[0])/bounds.height*relmousepos.y + t.y[0]
});
}
}
resetTilt() {
for (let key in this.DOM.tilt ) {
TweenMax.to(this.DOM.tilt[key], 2, {
ease: Elastic.easeOut.config(1,0.4),
x: 0,
y: 0
});
}
}
/**
* Скрываем элементы
* - Изменение размера и затухание изображения и элементов на заднем фоне
* - Перемещение вниз и затухание заголовка и цифр
*/
hide(withAnimation = true) { this.toggle(withAnimation,false); }
/**
* Сброс.
*/
show(withAnimation = true) { this.toggle(withAnimation); }
toggle(withAnimation, show = true) {
TweenMax.to(this.DOM.tilt.img, withAnimation ? 0.8 : 0, {
ease: Expo.easeInOut,
delay: !withAnimation ? 0 : show ? 0.15 : 0,
scale: show ? 1 : 0,
opacity: show ? 1 : 0,
});
TweenMax.to(this.DOM.bg, withAnimation ? 0.8 : 0, {
ease: Expo.easeInOut,
delay: !withAnimation ? 0 : show ? 0 : 0.15,
scale: show ? 1 : 0,
opacity: show ? 1 : 0
});
this.toggleTexts(show ? 0.45 : 0, withAnimation, show);
}
// скрываем текст (Анимация вниз и затухание).
hideTexts(delay = 0, withAnimation = true) { this.toggleTexts(delay, withAnimation, false); }
// показываем тексты (сброс анимации и плавное появление).
showTexts(delay = 0, withAnimation = true) { this.toggleTexts(delay, withAnimation); }
toggleTexts(delay, withAnimation, show = true) {
TweenMax.to([this.DOM.tilt.title, this.DOM.tilt.number], !withAnimation ? 0 : show ? 1 : 0.5, {
ease: show ? Expo.easeOut : Quart.easeIn,
delay: !withAnimation ? 0 : delay,
y: show ? 0 : 20,
opacity: show ? 1 : 0
});
}
}
Класс содержимого. Представляет элемент содержимого сетки
class Content {
constructor(el) {
this.DOM = {el: el};
// Элементы: изображение, заголовок, подзаголовок и текст.
this.DOM.img = this.DOM.el.querySelector('.content__item-img');
this.DOM.title = this.DOM.el.querySelector('.content__item-title');
this.DOM.subtitle = this.DOM.el.querySelector('.content__item-subtitle');
this.DOM.text = this.DOM.el.querySelector('.content__item-text');
// Делит заголовок на span'ы, используя библиотеку charming.js
charming(this.DOM.title);
// И получаем доступ к span (буквам)
this.DOM.titleLetters = this.DOM.title.querySelectorAll('span');
this.titleLettersTotal = this.DOM.titleLetters.length;
}
/**
* Показаем / скрываем элементы (заголовоки, подзаголовки и текст).
*/
show(delay = 0, withAnimation = true) { this.toggle(delay, withAnimation); }
hide(delay = 0, withAnimation = true) { this.toggle(delay, withAnimation, false); }
toggle(delay, withAnimation, show = true) {
setTimeout(() => {
this.DOM.titleLetters.forEach((letter,pos) => {
TweenMax.to(letter, !withAnimation ? 0 : show ? .6 : .3, {
ease: show ? Back.easeOut : Quart.easeIn,
delay: !withAnimation ? 0 : show ? pos*.05 : (this.titleLettersTotal-pos-1)*.04,
startAt: show ? {y: '50%', opacity: 0} : null,
y: show ? '0%' : '50%',
opacity: show ? 1 : 0
});
});
this.DOM.subtitle.style.opacity = show ? 1 : 0;
this.DOM.text.style.opacity = show ? 1 : 0;
}, withAnimation ? delay*1000 : 0 );
}
}
Сетка
// Сетка.
class Grid {
constructor(el) {
this.DOM = {el: el};
// обертка
this.DOM.gridWrap = this.DOM.el.parentNode;
// элементы
this.items = [];
Array.from(this.DOM.el.querySelectorAll('.grid__item')).forEach(itemEl => this.items.push(new GridItem(itemEl)));
// Суммарное количество элементов
this.itemsTotal = this.items.length;
// Содержимое элементов
this.contents = [];
Array.from(document.querySelectorAll('.content > .content__item')).forEach(contentEl => this.contents.push(new Content(contentEl)));
// Back control and scroll indicator (elements shown when the item´s content is open). Кнопка закрытия и scroll (показываем элементы, когда контентент в режиме open)
this.DOM.closeCtrl = document.querySelector('.content__close');
this.DOM.scrollIndicator = document.querySelector('.content__indicator');
// Отрываем элемент сетки
this.current = -1;
// Инициализируем / привязываем события
this.initEvents();
}
initEvents() {
// Клик по элементу сетки, прячет все остальные элементы (упорядоченны по близости к нажатому).
// и разворачивает/открывает тот, по которому был совершен клик
for (let item of this.items) {
item.DOM.el.addEventListener('click', (ev) => {
ev.preventDefault();
this.openItem(item);
});
}
// Закрытие элемента
this.DOM.closeCtrl.addEventListener('click', () => this.closeItem());
// Незакончено! Сейчас: если есть открытый элемент, то показываем сетку.
this.resizeFn = () => {
if (this.current === -1 || winsize.width === window.innerWidth) return;
this.closeItem(false);
};
window.addEventListener('resize', this.resizeFn);
}
getSizePosition(el, scrolls = true) {
const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
return {
width: el.offsetWidth,
height: el.offsetHeight,
left: scrolls ? getOffset(el, 'left')-scrollLeft : getOffset(el, 'left'),
top: scrolls ? getOffset(el, 'top')-scrollTop : getOffset(el, 'top')
};
}
openItem(item) {
if ( this.isAnimating ) return;
this.isAnimating = true;
// Получаем текущую позицию прокрутки.
this.scrollPos = window.scrollY;
// Отключаем прокрутку страниц.
disableScroll();
// Отключаем наклон.
allowTilt = false;
// Задаем текущее значение (индекс выбранного элемента).
this.current = this.items.indexOf(item);
// Скрываю все элементы сетки, кроме того, который мы хотим открыть.
this.hideAllItems(item);
// Также скрываю элементы текста.
item.hideTexts();
// Устанавливаю для элементов самый высокий z-index, чтобы они перекрывали любой другой элемент сетки
item.DOM.el.style.zIndex = 1000;
// Получаю ширину и высоту" grid__item-bg " и задать ее явно,
// Также установливаю его top и left соответственно странице.
const itemDim = this.getSizePosition(item.DOM.el);
item.DOM.bg.style.width = `${itemDim.width}px`;
item.DOM.bg.style.height = `${itemDim.height}px`;
item.DOM.bg.style.left = `${itemDim.left}px`;
item.DOM.bg.style.top = `${itemDim.top}px`;
// Устанавливаю position: fixed
item.DOM.bg.style.position = 'fixed';
// Рассчитываем диагональ окна просмотра. Это нужно будет учитывать при масштабировании элемента items bg.
const d = Math.hypot(winsize.width, winsize.height);
// Масштабирование элемента item bg
TweenMax.to(item.DOM.bg, 1.2, {
ease: Expo.easeInOut,
delay: 0.4,
x: winsize.width/2 - (itemDim.left+itemDim.width/2),
y: winsize.height/2 - (itemDim.top+itemDim.height/2),
scaleX: d/itemDim.width,
scaleY: d/itemDim.height,
rotation: -1*item.angle*2
});
// Получаю контент элемента соответсвующего текущему элементу сетки
const contentEl = this.contents[this.current];
// Задаю класс current
contentEl.DOM.el.classList.add('content__item--current');
// Расчитываю размеры и позиции элементов изображения и контента
// Нам это нужно, чтобы мы могли масштабировать и переводить изображение элементов в тот же размер и положение
const imgDim = this.getSizePosition(item.DOM.imgWrap);
const contentImgDim = this.getSizePosition(contentEl.DOM.img, false);
// Показываем кнопку "назад" и индикатор прокрутки и все элементы контента (1 секунда задрежки)
this.showContentElems(contentEl, 1);
// Анимаруем изображение элемента
TweenMax.to(item.DOM.tilt.img, 1.2, {
ease: Expo.easeInOut,
delay: 0.55,
scaleX: contentImgDim.width/imgDim.width,
scaleY: contentImgDim.height/imgDim.height,
x: (contentImgDim.left+contentImgDim.width/2)-(imgDim.left+imgDim.width/2),
y: (contentImgDim.top+contentImgDim.height/2)-(imgDim.top+imgDim.height/2),
rotation: 0,
onComplete: () => {
// Скрываем изображение элемента и показываем изображение содержимого. Они должны перекрываться
item.DOM.tilt.img.style.opacity = 0;
contentEl.DOM.img.style.visibility = 'visible';
// Главной контентной обертке устанавливаем poosition: absolute
contentEl.DOM.el.parentNode.style.position = 'absolute';
// Прячем scroll у сетки
this.DOM.gridWrap.classList.add('grid-wrap--hidden');
// Поднимаем страницу (скроллим вверх)
window.scrollTo(0, 0);
// Разрешаем скролл страницы
enableScroll();
this.isAnimating = false;
}
});
}
closeItem(withAnimation = true) {
if ( this.isAnimating ) return;
this.isAnimating = true;
// Получаю контент элемента, соответствующего этому элементу сетки.
const contentEl = this.contents[this.current];
// Скроллим до предыдущей позиции перед открытием элемента.
window.scrollTo(0, this.scrollPos);
contentEl.DOM.el.parentNode.style.position = 'fixed';
// Отключаем прокрутку страницы
disableScroll();
// Показываю скролл сетки
this.DOM.gridWrap.classList.remove('grid-wrap--hidden');
// Открытый элемент
const item = this.items[this.current];
// Скрываю кнопку назад, индикатор прокрутки и весь контент элемента
this.hideContentElems(contentEl, 0, withAnimation);
// Установите изображение сетки обратно (opacity: 1)
item.DOM.tilt.img.style.opacity = 1;
contentEl.DOM.img.style.visibility = 'hidden';
// Анимация изображения сетки обратно
TweenMax.to(item.DOM.tilt.img, withAnimation ? 1.2 : 0, {
ease: Expo.easeInOut,
scaleX: 1,
scaleY: 1,
x: 0,
y: 0,
rotation: item.angle*2
});
// И тоже самое с задним фоном
TweenMax.to(item.DOM.bg, withAnimation ? 1.2 : 0, {
ease: Expo.easeInOut,
delay: 0.15,
x: 0,
y: 0,
scaleX: 1,
scaleY: 1,
rotation: 0,
onComplete: () => {
contentEl.DOM.el.classList.remove('content__item--current');
item.DOM.bg.style.position = 'absolute';
item.DOM.bg.style.left = '0px';
item.DOM.bg.style.top = '0px';
this.current = -1;
allowTilt = true;
item.DOM.el.style.zIndex = 0;
enableScroll();
this.isAnimating = false;
}
});
// Показываем все элементы, кроме того который мы хотим скрыть
this.showAllItems(item, withAnimation);
// Также показываем тексты элемента (1 секунда задержки)
item.showTexts(1, withAnimation);
}
/**
* Переключение элементов контента.
*/
showContentElems(contentEl, delay = 0, withAnimation = true) { this.toggleContentElems(contentEl, delay, withAnimation); }
hideContentElems(contentEl, delay = 0, withAnimation = true) { this.toggleContentElems(contentEl, delay, withAnimation, false); }
toggleContentElems(contentEl, delay, withAnimation, show = true) {
// Переключение кнопки назад и индикатора прокрутки
TweenMax.to([this.DOM.closeCtrl, this.DOM.scrollIndicator], withAnimation ? 0.8 : 0, {
ease: show ? Expo.easeOut : Expo.easeIn,
delay: withAnimation ? delay : 0,
startAt: show ? {y: 60} : null,
y: show ? 0 : 60,
opacity: show ? 1 : 0
});
if ( show ) {
contentEl.show(delay, withAnimation);
}
else {
contentEl.hide(delay, withAnimation);
}
}
// Взято с https://stackoverflow.com/q/25481717
sortByDist(refPoint, itemsArray) {
let distancePairs = [];
let output = [];
for(let i in itemsArray) {
const rect = itemsArray[i].DOM.el.getBoundingClientRect();
distancePairs.push([distance(refPoint,{x:rect.left+rect.width/2, y:rect.top+rect.height/2}), i]);
}
distancePairs.sort((a,b) => a[0]-b[0]);
for(let p in distancePairs) {
const pair = distancePairs[p];
output.push(itemsArray[pair[1]]);
}
return output;
}
/**
* Показывает / скрывает все элементы сетки, кроме "исключенного" элемента.
* Элементы будут отсортированы на основе расстояния до элемента исключения.
*/
showAllItems(exclude, withAnimation = true) { this.toggleAllItems(exclude, withAnimation); }
hideAllItems(exclude, withAnimation = true) { this.toggleAllItems(exclude, withAnimation, false); }
toggleAllItems(exclude, withAnimation, show = true) {
if ( !withAnimation ) {
this.items.filter(item => item != exclude).forEach((item, pos) => item[show ? 'show' : 'hide'](withAnimation));
}
else {
const refrect = exclude.DOM.el.getBoundingClientRect();
const refPoint = {
x: refrect.left+refrect.width/2,
y: refrect.top+refrect.height/2
};
this.sortByDist(refPoint, this.items.filter(item => item != exclude))
.forEach((item, pos) => setTimeout(() => item[show ? 'show' : 'hide'](), show ? 300+pos*70 : pos*70));
}
}
}
Инициализация
// Управляет тем, будет ли элемент иметь "наклон" при наведении (mousemove) или нет.
let allowTilt = true;
// Кэширование некоторых вещей..
const body = document.body;
const docEl = document.documentElement;
// Размеры окна
let winsize;
const calcWinsize = () => winsize = {width: window.innerWidth, height: window.innerHeight};
calcWinsize();
window.addEventListener('resize', calcWinsize);
// Инициализация сетки
const grid = new Grid(document.querySelector('.grid'));
// Предзагрузка картинок
imagesLoaded(document.querySelectorAll('.grid__item-img'), () => {
body.classList.remove('loading');
var msnry = new Masonry( grid.DOM.el, {
// Настройки
itemSelector: '.grid__item',
columnWidth: 260,
gutter: 100,
fitWidth: true
});
});
}
Оригинал статьи находится здесь: https://tympanus.net/codrops/2018/09/19/grid-layout-scrollable-content-view/
Демо: Посмотреть
Исходник: Посмореть/Скачать
Облако тегов
Следующая статья
Заработок в интернете, доступный каждому: веб-дизайн
Работа в интернете имеет огромное количество плюсов, поэтому всё больше и больше людей каждый год пытаются стать фрилансерами. К сожалению, у многих из них так ничего и не получается из-за банального отсутствия понимания того, что им необходимо делать. А существует несколько проверенных способов заработка в интернете, которые любой человек, не обладающий никакими специальными знаниями, сможет освоить. О самом прибыльном из них в этой статье и пойдёт речь.