Воссоздаем эффект подгрузки сеткой в стиле “Design Samsung”

Мы не будем загружать изображения динамически, вместо этого мы будем симулировать их появление при прокрутке. Разумеется, в реальных проектах при прокрутке нужно будет использовать что-то вроде ленивой загрузки или бесконечной прокрутки.

Заметьте, что наш пример ориентирован только на современные браузеры!

Итак, приступим.

Разметка


Для сетки будем использовать неупорядоченный список, и обертку над ним. У первого элемента списка будут особые стили, так что мы зададим ему класс “title-box”
Код
<section class="grid-wrap">
  <ul class="grid swipe-right" id="grid">
  <li class="title-box">
  <h2>Illustrations by <a href="http://ryotakemasa.com/">Ryo Takemasa</a></h2>
  </li>
  <li><a href="#"><img src="img/1.jpg" alt="img01"><h3>Kenpo News April 2014 issue</h3></a></li>
  <li><a href="#"><img src="img/2.jpg" alt="img02"><h3>SQUET April 2014 issue</h3></a></li>
  <li><!-- ... --></li>
  <!-- ... -->
  </ul>
</section>

Каждый элемент списка будет содержать якорь с изображением и заголовком. Отмечу, что тип анимации мы будем контролировать заданием списку одного из трех классов: swipe-right, swipe-down или swipe-rotate.

При загрузке страницы необходимо, чтобы часть элементов, попадающая в область видимости, уже была видна, а при прокрутке запускалась анимация на других элементах. Это будет сделано с помощью добавления элементу класса animate для непосредственно загружаемых элементов списка. Изначально видимые элементы будут иметь класс shown, также как уже загруженные элементы, для которых завершилась анимация.

Цветной занавес, который будет выезжать до проявления изображения, будет добавляться динамически. Будем использовать div, который мы добавим в якорь сразу после заголовка. Этот блок будет иметь класс curtain, и цветом фона для него будет основной цвет изображения. Его мы будем получать с помощью скрипта ColorFinder.

Давайте перейдем к стилям.

CSS


CSS в статье не включает в себя вендорных префиксов, но их можно найти в исходниках.

Для начала стилизуем основной контейнер, чью ширину мы ограничим до 1260px (так, чтобы в ряд помещалось максимум четыре изображения).
Код
.grid-wrap {
  clear: both;
  margin: 0 auto;
  padding: 0;
  max-width: 1260px;
}

Неупорядоченный список будет отцентрирован, для него будут сброшены стили по умолчанию:
Код
.grid {
  margin: 30px auto;
  padding: 0;
  min-height: 500px;
  list-style: none;
}

Если у нас включен JavaScript, то сетка должна контролироваться нашим скриптом. Будем использовать класс loaded, который мы установим блоку, как только будет готова сетка изображений, чтобы иметь возможность управлять индикатором загрузки и видимостью элементов.
Код
.js .grid {
  background: url(../img/loading.gif) no-repeat 50% 100px;
}

.js .grid.loaded {
  background: none;
}

Делаем мы это для того, чтобы изображения не отображались до тех пор, пока они не будут загружены полностью.

Если включен JavaScript - элементы списка будут в плавающем состоянии, с равнением по левому краю, шириной в 314px (ширина изображения плюс ширина отступа). Если JavaScript отключен, то мы элементы будут строчными с выравниванием по верху.
Код
.grid li {
  display: inline-block;
  overflow: hidden;
  width: 314px;
  text-align: left;
  vertical-align: top;
}

.js .grid li {
  display: none;
  float: left;
}

.js .grid.loaded li {
  display: block;
}

Давайте зададим особые стили заголовку:
Код
.title-box h2 {
  display: block;
  margin: 7px;
  padding: 20px;
  background: #2E3444;
  color: #D3EEE2;
  text-transform: uppercase;
  letter-spacing: 1px;
  font-weight: 300;
}

.title-box h2 a {
  display: block;
  font-weight: 900;
}

.title-box h2 a:hover {
  color: #D3EEE2;
}

Также немного стилизуем якорь и изображение:
Код
.grid li > a,
.grid li img {
  display: block;
  outline: none;
  border: none;
}

У якоря контроль переполнения нужно выставить в скрытие, так как мы хотим двигать цветной занавес без того, чтобы он выглядывал за границы блока.
Код
.grid li > a {
  position: relative;
  overflow: hidden;
  margin: 7px;
}

Элемент curtain будет спозиционирован абсолютно, его ширина и высота должны быть 100%.
Код
.grid .curtain {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #96cdc8;
}

Занавес будет находится поверху изображения и заголовка, что необходимо для работы нашего эффекта.

Три наших эффекта будут заставлять выезжать занавес: слева направо, сверху вниз, или поворачиваться из нижнего левого угла:
Код
.grid.swipe-right .curtain {
  transform: translate3d(-100%,0,0);
}

.grid.swipe-down .curtain {
  transform: translate3d(0,-100%,0);
}

.grid.swipe-rotate .curtain {
  width: 200%;
  height: 200%;
  transform: rotate3d(0,0,1,90deg);
  transform-origin: top left;
}

Для эффекта поворота мы удваиваем размеры занавеса, чтобы при повороте не были видны углы блока.

В дополнение, к занавесу мы привяжем псевдо-элемент, который будет играть роль тени, которая будет перекрывать изображение. В зависимости от эффекта, псевдо-элемент будет находиться слева или поверх занавеса:
Код
.grid .curtain::after {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,1);
  content: '';
}

.grid.swipe-right .curtain::after,
.grid.swipe-rotate .curtain::after {
  left: -100%;
}

.grid.swipe-down .curtain::after {
  top: -100%;
}

Для заголовка зададим темный фон, он будет спозиционирован абсолютно:
Код
.grid li h3 {
  position: absolute;
  bottom: 0;
  left: 0;
  margin: 0;
  padding: 20px;
  width: 100%;
  background: #2E3444;
  color: #D3EEE2;
  text-align: right;
  text-transform: uppercase;
  letter-spacing: 1px;
  font-weight: 800;
  font-size: 1em;
  transition: transform 0.2s, color 0.2s;
}

Для эффекта при наведении курсора, обыграем псевдо-элемент якоря ::before. Он будет спозиционирован абсолютно, и при наведении будет анимировать толщину его границ. Заголовок будет слегка смещаться, и изменять цвет текста на белый:
Код
.grid li > a::before {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100.5%;
  border: 0px solid transparent;
  background: transparent;
  content: '';
  transition: border-width 0.2s, border-color 0.2s;
}

/* Эффекты наведения */
.grid li.shown:hover h3 {
  color: #fff;
  transform: translate3d(0,-30px,0);
}

.grid li.shown:hover > a::before {
  border-width: 14px;
  border-color: #2E3444;
}

Теперь давайте разберемся с анимациями.

Как было сказано ранее, для занавеса мы задаем изначально состояние “скрыт”, зависящее от типа анимации.

При прокрутке страницы, и попадании элемента в область просмотра, ему назначается класс animate, что запускает воспроизведение анимации.

Для эффекта скольжения слева направо (который можно наблюдать на странице Samsung), сместим его позицию до 0, что заставит его сместиться слева в центр, а после сместим его дальше вправо. Тем, что мы удерживаем смещение 0 в промежутке между 50% и 60% анимации, элемент дольше задерживается в поле зрения, а не просто смещается слева направо:
Код
/* Сдвиг вправо */
.grid.swipe-right li.animate .curtain {
  animation: swipeRight 1.5s cubic-bezier(0.6,0,0.4,1) forwards;
}

@keyframes swipeRight {
  50%, 60% { transform: translate(0); }
  100% { transform: translate3d(100%,0,0); }
}

Почему мы используем здесь translate(0)? Из-за того, что у некоторых браузеров, вроде IE11, есть проблемы с translate3d(0,0,0) в этом контексте.

Эффект смещения вниз почти такой же, только мы используем ось Y вместо оси X:-
Код
/* Сдвиг вниз */
.grid.swipe-down li.animate .curtain {
  animation: swipeDown 1.5s cubic-bezier(0.6,0,0.4,1) forwards;
}

@keyframes swipeDown {
  50%, 60% { transform: translate(0); }
  100% { transform: translate3d(0,-100%,0); }
}

Эффект разворота следует тем же принципам, просто вместо смещения мы поворачиваем элемент:
Код
/* Сдвиг с поворотом */
.grid.swipe-rotate li.animate .curtain {
  animation: swipeRotate 1.5s ease forwards;
}

@keyframes swipeRotate {
  50%, 60% { transform: rotate3d(0,0,1,0deg); }
  100% { transform: rotate3d(0,0,1,-90deg); }
}

Псевдо-элемент с тенью будет исчезать, начиная с момента, когда занавес начнет отъезжать, открывая изображение:
Код
.grid li.animate .curtain::after {
  animation: fadeOut 1.5s ease forwards;
  animation-delay: inherit;
}

@keyframes fadeOut {
  50%, 60% { opacity: 1; }
  100% { opacity: 0; }
}

Так как мы будем менять задержки анимации в скрипте, необходимо убедиться, что псевдо-элементам будут установлены те же значения задержек, что и родительским. Этого можно добиться, выставив псевдо-элементам свойство animation-delay: inherit. Если всем нашим элементам можно легко выставить нужные задержки посредством CSS, то манипуляцией псевдо-элементами посредством JavaScript могут возникнуть проблемы.

И последним шагом нужно сделать так, чтобы изображение и заголовок изначально были спрятаны, и показывать их только тогда, когда пройдет 60% анимации. С использованием шаговой функции step-end (которая является эквивалентом steps(1, end)) можно контролировать видимость элементов в необходимый момент времени. Необходимо использовать те же самые анимации, что были определены ранее, так чтобы отобразить элемент на 60% анимации, как раз тогда, когда занавес начинает отъезжать, открывая изображение:
Код
.js .grid li img,
.js .grid li h3 {
  visibility: hidden;
}

.grid li.animate img,
.grid li.animate h3 {
  animation: showMe 1.5s step-end forwards;
}

@keyframes showMe {
  from { visibility: hidden; }
  60%, 100% { visibility: visible; }
}

.grid li.shown img,
.grid li.shown h3 {
  visibility: visible;
}

Давайте взглянем на JavaScript.

JavaScript


Чего нам необходимо добиться - так это того, чтобы наши элементы отображались при попадании в область просмотра. Каждому появляющемуся элементу будет назначаться класс, который будет запускать анимацию. На первых элементах сетки нам нет необходимости показывать анимацию, так что им назначим класс show. Также нам нужно определить основной цвет изображения, чтобы мы могли установить цвет фона для занавеса.

Начнем с опций скрипта. minDelay и maxDelay определяют диапазон задержек, которые будет длиться каждая из наших анимаций (мы выбираем случайное число из этих значений). Это гарантирует, что анимация на каждом отдельном элементе будет немного отличаться от других, из-за чего эффект смотрится гораздо приятнее. Если вы хотите, чтобы все анимации начинались в одно и то же время, просто установите опцию maxDelay в 0. viewportFactor определяет, какой процент элемента должен попасть в область просмотра, чтобы для него была запущена анимация. Так 0 (0%) означает, что анимация запускается, как только элемент хоть какой-то частью попадает в область просмотра. 1 (100%) означает, что элемент должен целиком войти в область просмотра, чтобы была запущена анимация.
Код
GridScrollFx.prototype.options = {
  minDelay : 0,
  maxDelay : 500,
  viewportFactor : 0
}

Давайте перейдем к инициализации и кешированию некоторых переменных, а также к инициализации Masonry. Изображения должны быть загружены на страницу, чтобы плагин Masonry мог их корректно обработать.

Далее, при загрузке страницы необходимо различать те элементы, которые уже находятся в области просмотра, и те, которые туда еще не попали. Для тех, кто уже находится в области просмотра, нужно назначить класс show, чтобы они были видимы изначально, без анимации. Для тех изображений, которые не попадают в область просмотра, необходимо создать занавес, на котором будет проиграна анимация, как только скрытый элемент попадет в область просмотра. Также каждому элементу зададим задержку начала анимации.

И, наконец, установим обработчики на события прокрутки и изменения размера окна. Подробнее об этом будет рассказано позже.
Код
GridScrollFx.prototype._init = function() {
  var self = this, items = [];

  [].slice.call( this.el.children ).forEach( function( el, i ) {
  var item = new GridItem( el );
  items.push( item );
  } );

  this.items = items;
  this.itemsCount = this.items.length;
  this.itemsRenderedCount = 0;
  this.didScroll = false;

  imagesLoaded( this.el, function() {
  // отображаем сетку
  self.el.style.display = 'block';

  // инициализируем Masonry
  new Masonry( self.el, {
  itemSelector : 'li',
  isFitWidth : true,
  transitionDuration : 0
  } );

  // элементы, которые уже есть в области просмотра
  self.items.forEach( function( item ) {
  if( inViewport( item.el ) ) {
  ++self.itemsRenderedCount;
  classie.add( item.el, 'shown' );
  }
  else {
  item.addCurtain();
  // и случайная задержка
  item.changeAnimationDelay( Math.random() * ( self.options.maxDelay - self.options.minDelay ) + self.options.minDelay );
  }
  } );

  var onScrollFn = function() {
  if( !self.didScroll ) {
  self.didScroll = true;
  setTimeout( function() { self._scrollPage(); }, 200 );
  }

  if( self.itemsRenderedCount === self.itemsCount ) {
  window.removeEventListener( 'scroll', onScrollFn, false );
  }
  }

  // анимация элементов, попадающих в область просмотра (при прокрутке)
  window.addEventListener( 'scroll', onScrollFn, false );
  // проверяем, если после изменения размера окна в область просмотра попали новые элементы
  window.addEventListener( 'resize', function() { self._resizeHandler(); }, false );
  });
}

Заметьте, что мы создали отдельную функцию GridItem, чтобы держать в ней данные и методы отдельных элементов. При создании занавеса мы выставляем ему цвет фона. Этот цвет будет тем цветом, который наиболее распространен на соответствующем изображении. Получить его можно с помощью плагина ColorFinder:
Код
function GridItem( el ) {
  this.el = el;
  this.anchor = el.querySelector( 'a' )
  this.image = el.querySelector( 'img' );
  this.desc = el.querySelector( 'h3' );
}

GridItem.prototype.addCurtain = function() {
  if( !this.image ) return;
  this.curtain = document.createElement( 'div' );
  this.curtain.className = 'curtain';
  var rgb = new ColorFinder( function favorHue(r,g,b) {
  // exclude white
  //if (r>245 && g>245 && b>245) return 0;
  return (Math.abs(r-g)*Math.abs(r-g) + Math.abs(r-b)*Math.abs(r-b) + Math.abs(g-b)*Math.abs(g-b))/65535*50+1;
  } ).getMostProminentColor( this.image );
  if( rgb.r && rgb.g && rgb.b ) {
  this.curtain.style.background = 'rgb('+rgb.r+','+rgb.g+','+rgb.b+')';
  }
  this.anchor.appendChild( this.curtain );
}

GridItem.prototype.changeAnimationDelay = function( time ) {
  if( this.curtain ) {
  this.curtain.style.WebkitAnimationDelay = time + 'ms';
  this.curtain.style.animationDelay = time + 'ms';
  }
  if( this.image ) {
  this.image.style.WebkitAnimationDelay = time + 'ms';
  this.image.style.animationDelay = time + 'ms';
  }
  if( this.desc ) {
  this.desc.style.WebkitAnimationDelay = time + 'ms';
  this.desc.style.animationDelay = time + 'ms';
  }
}

Давайте посмотрим, что происходит при прокрутке страницы (заметьте, что обработчик события прокрутки вызывается каждые 200 миллисекунд, чтобы избежать проблем с производительностью). Сначала, мы проходимся по всем нашим элементам, чтобы определить, какие из них в области просмотра, а какие еще не отображены, или какие еще находятся в процессе анимации. Если у элемента нет занавеса, тогда мы просто добавляем ему класс shown и выходим из обработчика, иначе добавляем ему класс animate, чтобы запустить анимацию. Например, первый элемент в нашей демонстрационной сетке будет одним из этих случаев. Как только анимация завершилась, мы добавим класс shown, и уберем класс animate.
Код
GridScrollFx.prototype._scrollPage = function() {
  var self = this;
  this.items.forEach( function( item ) {
  if( !classie.has( item.el, 'shown' ) && !classie.has( item.el, 'animate' ) && inViewport( item.el, self.options.viewportFactor ) ) {
  ++self.itemsRenderedCount;

  if( !item.curtain ) {
  classie.add( item.el, 'shown' );
  return;
  };

  classie.add( item.el, 'animate' );

  // по завершению анимации добавляем класс shown
  var onEndAnimationFn = function( ev ) {
  if( support.animations ) {
  this.removeEventListener( animEndEventName, onEndAnimationFn );
  }
  classie.remove( item.el, 'animate' );
  classie.add( item.el, 'shown' );
  };

  if( support.animations ) {
  item.curtain.addEventListener( animEndEventName, onEndAnimationFn );
  }
  else {
  onEndAnimationFn();
  }
  }
  });
  this.didScroll = false;
}

При изменении размеров окна необходимо проверить, если в область просмотра попали новые элементы:
Код
GridScrollFx.prototype._resizeHandler = function() {
  var self = this;
  function delayed() {
  self._scrollPage();
  self.resizeTimeout = null;
  }
  if ( this.resizeTimeout ) {
  clearTimeout( this.resizeTimeout );
  }
  this.resizeTimeout = setTimeout( delayed, 1000 );
}

Ну вот и все! Надеюсь, вам понравилась эта статья, и вы нашли ее полезной!

Обратите внимание, что если в Chrome нв Windows у вас включены экспериментальные возможности веб-платформ, эффект не будет виден (элементы будут просто появляться, без анимации)

  • FalleN

  • 2953

  • 1

  • 230

Ссылки на статью:

Похожие статьи: