Сегодня мы разберём, как реализуется эффект Explode без использования каких-либо дополнительных библиотек.

> Демо-страничка: Explode.js



Что требуется сделать:

  • Разрезать элемент на N*N ячеек
  • Поместить в каждую ячейку копию «взрываемого» элемента
  • Сместить margin-ами содержимое каждой ячейки
  • Анимировать каждую ячейку в нужном направлении

Разрезаем элемент

Это самая простая часть. Дано: некий элемент (например, картинка или блок с текстом) для взрывания и количество ячеек N*N. Пишем цикл, в котором будем создавать N*N дивов и помещать, а затем сдвигать клон нашего элемента.

Объявляем нужные для работы переменные:

function Explode( elem, N ) {
	N = N || 3;
	var pos = elem.getBoundingClientRect();
	var top = pos.top;
	var left = pos.left;
	var width = Math.round( elem.clientWidth / N );
	var height = Math.round( elem.clientHeight / N );
	// [...]

Создаём ячейки:

for ( var i = 0; i < N; i++ ) {
	for ( var j = 0; j < N; j++ ) {
		var tile = document.createElement( "DIV" );
		document.body.appendChild( tile );
		// [...]
	}
}

Задаём ячейке необходимые стили (этот код уже внутри цикла):

tile.style.position = "fixed";
tile.style.width = width + "px";
tile.style.height = height + "px";
tile.style.top = (top + height * i) + "px";
tile.style.left = (left + width * j) + "px";
// поскольку мы хотим показывать в ячейке лишь часть элемента,
// то скрываем всё что не влезает
tile.style.overflow = "hidden";

Теперь вставляем в каждую ячейку клон элемента:

var tileContent = elem.cloneNode( true );
tile.appendChild( tileContent );

Сейчас надо сдвинуть клон в каждой ячейке, чтобы совокупность всех ячеек выглядела точно так же, как цельная картинка (сейчас же в каждой ячейке просто её верхний левый угол).

tileContent.style.marginTop = (-height * i - pos.top) + "px";
tileContent.style.marginLeft = (-width * j - pos.left) + "px";

Т.е. сдвигаем вверх на высоту ячейки, помноженную на номер ячейки по вертикали. Аналогично сдвигаем влево. -pos.top и -pos.left нужны на случай, если элемент сдвинут относительно края окна (а он, скорее всего, сдвинут). Если не отнять эти величины, то в ячейке будет пустота либо не тот кусок элемента — так как учитываются и «родные» отступы.

Первая половина задачи выполнена — картинка «разрезана» на ячейки, с каждой из которых можно работать по отдельности (а именно анимировать в разные стороны), но при этом для пользователя картинка выглядит без изменений.

Теперь надо определить, в каком направлении анимировать каждую из ячеек. Т.е. верхняя левая должна улетать вверх и влево, ячейка справа от неё — просто вверх и т.д., тем самым создавая эффект «взрыва». Надо бы написать вспомогательную функцию direction(i, j, N), где i и j — номер ячейки (в цикле), а N — кол-во ячеек по вертикали и горизонтали.

Попытка №1

Сначала была идея возвращать некую цифру (например, 0 — вверх, 1 — вверх и вправо и т.д. по часовой стрелке) и по ней определять направление. Это на первый взгляд показалось удобным и интуитивным, но на деле породило кучу повторяющегося кода в стиле:

function direction (i, j, N) {
	if ( i == 0 && j == 0 ) return 7; // влево и вправо
	if ( i == 0 && j < N - 1 ) return 0; // только вверх
	if ( i == 0 && j == N - 1 ) return 1; // вверх и влево
	// [...]

Я даже боюсь представить, как это анимировать. Писать if для каждого случая? Нет, нужно что-то другое.

Этот способ имеет два недостатка:

— громоздкость
— не совсем корректные значения. Например, при N=5 верхняя левая ячейка анимируется влево и вверх, тут всё правильно, однако следующая справа от неё ячейка будет улетать строго вверх, хотя хотелось бы вверх и немного влево (но не так сильно)

Попытка №2

Создать двумерный массив N*N, заполнить его нулями, назначить элементу (i, j) значение 1, и «искать» его, рекурсивно срезая с массива по одному слою (т.е. делая из массива 5х5 массив 4х4 и так далее). Очевидный плюс — нет кучи if-ов и громоздкого кода. Минус всё тот же — не совсем нужные значения.

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

Попытка №3, успешная

Векторы. Это компактное и удобное решение, которое также решает проблему с «не совсем корректными значениями». Мы будем узнавать, как далеко ячейка находится от центра, и вычислять некий вектор (x, y), и основываясь на x и y, анимировать ячейку по направлениям.

function direction( i, j, N) {
	return [ (N-1) / 2 - i, j - (N-1) / 2 ]
};

Как же всё было просто…

Для большей наглядности — какие значения задаются ячейкам при N=7:

image

Дело осталось за малым: анимировать каждую ячейку в заданном направлении. В теле цикла вызываем вспомогательную функцию animateStart для каждой ячейки:

animateStart( tile, left + width * j, top + height * i, direction( i, j, N ) );

где первым аргументом передаётся сама ячейка, вторым и третьим — её начальные координаты, а последним — вектор (x, y), который представлен массивом из двух чисел.

Теперь приступим к написанию функции animateStart.

function animateStart( elem, posX, posY, dir ) {
	var vY = dir[0]; // вектор по вертикали
	var vX = dir[1]; // вектор по горизонтали
	var start = new Date().getTime(); // время начала анимации
	// toX и toY - конечные координаты
	// например, у верхней левой ячейки вектор (3, -3), таким образом
	// в этом примере она сдвинется на 150 пикселей вверх и
	// на столько же влево
	var toY = posY - 50 * vY;
	var toX = posX + 50 * vX;
	setTimeout( animate, 10 ); // запускаем анимацию
	// [...]

Теперь напишем функцию animate. Это — прогресс анимации. Она будет запускаться каждые 10 миллисекунд, ненамного сдвигая ячейку по направлениям. Так и создаётся эффект плавной анимации.

function animate () {
	// прогресс анимации, в начале - 0, в конце - 1
	var m = (new Date().getTime() - start) / 500;
	if (m > 1) m = 1;
	elem.style.top = (posY + (toY - posY) * m) + "px"; // сдвигаем ячейку
	elem.style.left = (posX + (toX - posX) * m) + "px"; // по стандартной формуле
	elem.style.opacity = 1 - m; // а также делаем эффект "затухания", добавляя прозрачность
	if (m < 1) setTimeout( animate, 10 ); // если прогресс анимации меньше 1, запускаем функцию снова
	}

Последний штрих: удаляем оригинальный элемент.

elem.parentNode.removeChild( elem );

Плагин готов! Демо-страничка в самом начале статьи, а js-исходник находится тут.
Поделиться с друзьями
-->

Комментарии (17)


  1. andreymal
    10.02.2017 13:25
    +7

    В 2017 году и без CSS3 transition+transform? Это ж фи, на мобильниках тормозит.


  1. MrCheater
    10.02.2017 13:27
    +8

    Хочу дать несколько советов:


    • Не учите людей анимации через setTimeout делать, есть же requestAnimationFrame
    • Используйте для статей на Хабре тег <oembed>, чтобы все могли смотреть ваши демки, не отходя от кассы

    Пример использования OEMBED
    <oembed>http://codepen.io/MrCheater/pen/QGqdbo</oembed>


  1. wwwamn
    10.02.2017 17:22
    +1

    Кодировка, Карл, кодировка… В демо пропиши в или отправь заголовком.


  1. avdept
    10.02.2017 17:44

    А кто будет чистить DOM после окончания анимации?


    1. DiphenylOxalate
      13.02.2017 22:52

      Так ведь потом можно «собрать» элемент обратно, если надо.
      В jquery explode такая возможность есть.


  1. vitvad
    10.02.2017 21:14
    +3

    <oftop>кто такая Анна и за что мы ее так?</oftop>


    1. CyberAP
      13.02.2017 11:19

      АннаСофия Робб


    1. Andrew_Pinkerton
      13.02.2017 22:51

      Аннасофи?я Робб (англ. AnnaSophia Robb, род. 8 декабря 1993, Денвер) — американская актриса. Wiki


  1. askd
    13.02.2017 17:17
    +1

    Pure CSS на коленке http://codepen.io/askd/full/ZLVqOP/


    1. bano-notit
      14.02.2017 22:01

      Ну не pure конечно, а PostCSS, но всё же работает)


      1. askd
        15.02.2017 09:59

        PostCSS только потому, что было лень писать 64 строки правил вручную. На выходе (в браузере) это всё же обычный CSS ;)


        1. bano-notit
          17.02.2017 22:17

          Ну… Когда на ангуляре 2 пишешь тоже в конце концов обычный js получается, так же как и со многими вещами.


  1. KostaShah
    13.02.2017 22:53

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


    1. andreymal
      13.02.2017 22:55

      Наверно, это возможно, но всё равно в сравнении с transform(translateX/Y) будет притормаживать


    1. askd
      15.02.2017 10:01

      Выше как раз такой пример в CodePen ;)


      1. KostaShah
        15.02.2017 10:42

        Да, верно, я заметил, и оценил :-) Отлично сделано, я примерно так и предполагал. Когда я писал, его не было (моё сообщение ждало модерации несколько дней).


        1. askd
          15.02.2017 11:08

          Модерация?! Ничего себе. Моё сразу опубликовалось (хотя у меня и плохая карма после Великого Обнуления)