Flipboard-анимация средствами CSS3 и JavaScript

в 16:58, , рубрики: animation, css3, flipboard, javascript, Веб-разработка, Песочница, метки: , , ,

Добрый день. Наверняка многие видели приложение для iOS под названием Flipboard. При всех его достоинствах, лично меня оно в первую очередь порадовало своими забавными анимациями перелистывания. Родилась идея реализовать нечто подобное для своего сайта исключительно на Javascript и CSS3.

В данном случае я использовал CSS3 свойство transform: rotate3d(...), которое требует поддержки аппаратного ускорения графики и адекватно работает только в Chrome 16+, поэтому все нужyые свойства я ограничил префиксами -webkit-. В продакшене для пользователей с неподходящим параметрами я заменял анимацию на более простую.

Вот так выглядит готовый результат:

Живое демо

Начнем с HTML. Мне хотелось сделать универсальный скрипт, который практически не будет зависеть от исходного html, поэтому в нём содержится только контейнер с нужным классом, изображения и ссылка для старта анимации:

<div class="flip-images">
		<img src="./images/image_1.jpg">
		<img src="./images/image_2.jpg">
		<img src="./images/image_3.jpg">
	</div>

	<a href="#" id='flip-one-more'>Flip</a>

Скрипт выполняет по большей части регуляторную функцию, но первой его задачей является парсинг, поместим url картинок в массив (для удобства использую jQuery):

var image_index = 0; //Индекс текущей картинки
	var images = parse_image_container();

	function collect_images(){
		var images = new Array();
		$('.flip-images img').each(function(){
			images.push($(this).attr('src'));
			$(this).remove();
		});
		return images;
	}

После того как функция collect_images() отработала, остался пустой контейнер с классом flip-images, нужно заполнить его html шаблоном, который будет содержать все необходимые слои. Каждый раз для анимации перелистывания необходимы две картинки, одна спереди, другая сзади, кроме того каждую картинку необходимо разделить на две части, плюс для пущей реалистичности нужно добавить псевдо-тени.

$('.flip-images').append("<div class='flip-container'>
				<div class='flip-top'>
					<div class='shadow-front'></div>
					<div class='front-image-top'></div>
					<div class='back-image-bottom'></div>
					<div class='shadow-back'></div>
				</div>
				<div class='back-image-top'></div>
				<div class='flip-bottom'>
					<div class='shadow-bottom'></div>
					<div class='front-image-bottom'></div>
				</div>
			      </div>");

В итоге будет переворачиваться контейнер с классом flip-top перекрывая контейнер flip-bottom.

Теперь осталось разложить нужные картинки в нужные контейнеры, эта роль отведена функции place_images():

function place_images(){
		
		var next_image_index = image_index+1;

		if(next_image_index==images.length){
			next_image_index = 0;
		}

		var front_image = "<img src='"+images[image_index]+"'>";
		var back_image = "<img src='"+images[next_image_index]+"'>";

		$('.back-image-top').html("").append(back_image);
		$('.back-image-bottom').html("").append(back_image);

		$('.front-image-top').html("").append(front_image);
		$('.front-image-bottom').html("").append(front_image);
	}

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

В скрипте осталось лишь повесить действие на ссылку для запуска анимации, но этим займемся чуть позже. Перейдем к CSS. Не буду расписывать каждый класс отдельно, они примитивны, лишь прокомментирую ключевые моменты.

.flip-images img{
	width: 300px;
	height: 200px;
}

.flip-container{
	width: 300px;
	height: 200px;
	position: relative;
	-webkit-perspective: 600px;
}

.flip-top{
	height: 100px;
	overflow: hidden;
	width: 300px;
}

.flip-bottom{
	position: relative;
	width: 300px;
	height: 100px;
}

/*  Переднее изображение  */

.front-image-top{ /*  Верхняя чать переднего изображения  */
	display: block;
	-webkit-backface-visibility: hidden; /*Делаем невидимой обратную сторону картинки*/
	position: absolute;
	top: 0px;
	width: 300px;
	height: 100px; /*Половиним высоту, чтобы была видна только верхняя часть изображения*/
	overflow: hidden; /*Скрываем нижнюю часть*/
	z-index: 900;
}

.front-image-bottom{ /*  Нижняя чать переднего изображения  */
	height: 100px;
	width: 300px;
	overflow: hidden;
	vertical-align: bottom;
	position: absolute;
	z-index: -2;
}

.front-image-bottom img{
	position: absolute;
	top: -100px; /*Смещаем картинку вверх на половину высоты, чтобы показать нижнюю часть*/
}

/*  Заднее изображение  */

.back-image-top{
	height: 100px;
	position: absolute;
	top: 0px;
	vertical-align: top;
	overflow: hidden;
	z-index: -1;
}

.back-image-bottom{
	display: block;
	position: absolute;
	top: 0px;
	height: 100px;
	-webkit-transform: rotateY(180deg) rotateZ(180deg);  /*Переворачиваем картинку вверх ногами и лицевой стороной назад*/
	overflow: hidden;
	width: 300px;
	-webkit-backface-visibility: hidden;
	z-index: 800;
}

.back-image-bottom img{
	position: absolute;
	top: -100px;
}

/*ТЕНИ*/

.shadow-top-front{
	position: absolute;
	background: #000;
	z-index: 1000;
	width: 300px;
	height: 100px;
	-webkit-backface-visibility: hidden;
	opacity: 0; /*Тень будет появляеться, поэтому сначала она невидима*/
}

.shadow-top-back{
	position: absolute;
	top: 0px;
	width: 300px;
	height: 100px;
	background: #000;
	z-index: 1000;
	-webkit-backface-visibility: hidden;
	-webkit-transform: rotateY(180deg); /*Разворачиваем тень лицом назад, чтобы при перевороте её не было видно*/
	opacity: 1; /*Тень будет исчезать, поэтому сначала она видна*/
}

.shadow-bottom{
	width: 300px;
	height: 100px;
	position: absolute;
	z-index: -1;
	background: #000;
	opacity: 0;
}

На данном этапе все изображения находятся на своих местах, осталось только задать анимации, для этого воспользуемся свойством keyframes.

Основная анимация будет называться flip и будет такой: верхняя часть изображения падает, отбивается от нижней части, взлетая немного вверх и снова падает до упора вниз. Так это будет выглядеть в CSS:

@-webkit-keyframes flip {
	0% {
		-webkit-transform: rotate3d(1,0,0, 0deg);
	}
	50% {
		-webkit-transform: rotate3d(1,0,0, -180deg);
	}
	60% {
		-webkit-transform: rotate3d(1,0,0, -155deg);
	}
	70% {
		-webkit-transform: rotate3d(1,0,0, -140deg);
	}
	100% {
		-webkit-transform: rotate3d(1,0,0, -180deg);
	}
}

Интервалы и значения поворота подобраны методом проб и ошибок. Аналогичным образом сделаны анимации и для теней:

@-webkit-keyframes shadowTopFront{
	0% { opacity: 0; }
	70% { opacity: 1; }
	100% { opacity: 0; }
}

@-webkit-keyframes shadowTopBack {
	0% { opacity: 0.8; }
	50% { opacity: 0; }
	60% { opacity: 0.05; }
	70% { opacity: 0.1; }
	100% { opacity: 0; }
}

@-webkit-keyframes shadowBottom {
	0% { opacity: 0; }
	50% { opacity: 0.6; }
	60% { opacity: 0.4; }
	70% { opacity: 0.3; }
	100% { opacity: 0.5; }
}

Осталось описать классы, которые запустят анимации:

.flip {
	/*background: #ccc;*/
	width: 200px;
	height: 100px;
	-webkit-transform-origin: bottom;
	-webkit-animation: flip 1s; /*Задаем имя и длительнось анимации*/
	-webkit-animation-iteration-count: 1; /*Количество итераций для заданной анимации*/
	-webkit-animation-timing-function: cubic-bezier(0,0,1,0.5); /*Задаем кривую Безье для просчета промежуточных значений анимированных параметров*/
	-webkit-transform: rotate3d(1,0,0, 180deg); /*Задаем конечное значение поворота, иначе по окончанию анимации плоскость вернется в 0 градусов*/
}

.shadow-top-front-animate{
	-webkit-animation: shadowTopFront 1s;
	-webkit-animation-iteration-count: 1;
	-webkit-animation-timing-function: cubic-bezier(0,0,1,0.5);
	opacity: 0;
}

.shadow-top-back-animate{
	-webkit-animation: shadowTopBack 1s;
	-webkit-animation-iteration-count: 1;
	-webkit-animation-timing-function: cubic-bezier(0,0,1,0.5);
	opacity: 0;
}

.shadow-bottom-animate{
	-webkit-animation: shadowBottom 1s;
	-webkit-animation-iteration-count: 1;
	-webkit-animation-timing-function: cubic-bezier(0,0,1,0.5);
	opacity: 1;
}

Вернёмся к javascript и зададим действие для клика по ссылке с id flip-one-more. По клику соответствующим контейнера должны подставляться нужные картинки, добавляться классы с анимациями и увеличиваться переменная image_index.

$('#flip-one-more').click(function(){
					
			place_images(image_index);

			$('.flip-top').addClass('flip');
			$('.shadow-top-front').addClass('shadow-top-front-animate');
			$('.shadow-top-back').addClass('shadow-top-back-animate');
			$('.shadow-bottom').addClass('shadow-bottom-animate');

			image_index = image_index + 1;
			if(image_index==images.length){
				image_index = 0;
			}
			return false;
		});

В таком виде все будет работать, но анимацию мы увидим только один раз, после повторных кликов по ссылке картинки будут просто переставляться. Все потому, что CSS-анимация — явление одноразовое, её нужно всегда возвращать в исходное положение. Увы, простым удалением присвоенных классов делу не поможешь, но существует один простой, вполне рабочий способ вернуть анимацию на исходную. Нужно удалить и снова добавить блоки, к которым была применена анимация. В итоге окончательный вариант функции для клика выглядит следующим образом:

$('#flip-one-more').click(function(){

        flip_container = $('.flip-container');
	    new_flip_container = flip_container.clone(true);
	    flip_container.before(new_flip_container);
	    $("." + flip_container.attr("class") + ":last").remove();

		place_images();

		$('.flip-top').addClass('flip');
		$('.shadow-top-front').addClass('shadow-top-front-animate');
		$('.shadow-top-back').addClass('shadow-top-back-animate');
		$('.shadow-bottom').addClass('shadow-bottom-animate');

		image_index++;
		if(image_index==images.length){
		     image_index = 0;
		}
	   return false;
});

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

Спасибо за внимание, понимаю, что код далёк от идеала, поэтому буду рад услышать конструктивную критику.

Демо

Архив с примером

Автор: snater

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


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