Занятие на вечер: эффект Explode на pure JS

в 10:07, , рубрики: animation, effects, javascript

Сегодня мы разберём, как реализуется эффект 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-исходник находится тут.

Автор: DiphenylOxalate

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js