Урок по реализации круглой кнопки загрузки (далее progress button) by Colin Garven. Будем использовать, описанную by Jake Archibald, технику анимации SVG линий для того, чтобы анимировать progress button и показать пользователю состояния «success and fail».

image

Сегодня мы покажем вам как создать изящную progress button. Это уникальный концепт submit button, предложенный by Colin Garven. Сперва взгляните на это (Demo ), постарайтесь придумать, как реализовать, и просто наслаждайтесь анимацией. Идея состоит в следующем: при первом клике submit button трансформируется в круг, который покажет анимацию загрузки, используя свои границы (далее бордер). Когда анимация закончится, кнопка вернет исходную форму и покажет отметку, указывающую, что подтверждение прошло успешно или нет.

Существует несколько способов реализовать кнопку с таким эффектом. Думая о реализации только посредствам CSS, самой тяжёлой частью кажется круг прогресса. Существует техника реализации с помощью свойства clip. Anders Ingemann написал отличный полный tutorial (на LESS). Но мы будем использовать технику, основанную на SVG, CSS transitions и немного JS. Что касается круга прогресса, отметок success /fail, мы воспользуемся техникой рисования SVG линий, описанную by Jake Archibald.

Стоит отметить, что анимирование SVG может быть проблематичным для некоторых браузеров. Так что воспринимайте этот tutorial только как экспериментальное упражнение.

Поехали!

The Master plan


Если вы внимательно изучили Colin’s Dribbble shot, то должны были заметить, что нам следует позаботиться о нескольких состояниях кнопки. Интересная часть – переход между двумя такими состояниями. Сперва мы должны показать простую кнопку с прозрачным фоном и цветным бордером. При наведении мышки кнопка заполняется цветом бордера, а текст становится белым.

image

При нажатии на кнопку (для того, что бы, например, подтвердить отправку формы), текст должен плавно исчезнуть, ширина кнопки — уменьшится до состояния круга, бордер — стать уже, и должна начаться анимация. Мы будем использовать SVG-круг для анимации, так что нам необходимо убедиться, что получившаяся кнопка такого же размера и в той же позиции, что и SVG-круг, который виден в этот момент. Затем мы рисуем круг, изображающий загрузку подтверждения.

image

Когда подтверждение завершено, а круг полностью нарисован, кнопка должна снова расшириться, нарисовать галочку в случае успешного подтверждения. И закрашиваем кнопку соответственно.

image

В случае неудачи подтверждения, нужно показать состояние ошибки.

image

Давайте создадим нашу разметку со всеми необходимыми элементами.

Разметка


Для разметки нам потребуется главный контейнер, кнопка со спаном(span), содержащим текст, и три SVG:

<!-- progress button -->
<div id="progress-button" class="progress-button">
	<!-- button with text -->
	<button><span>Submit</span></button>

	<!-- svg circle for progress indication -->
	<svg class="progress-circle" width="70" height="70">
		<path d="m35,2.5c17.955803,0 32.5,14.544199 32.5,32.5c0,17.955803 -14.544197,32.5 -32.5,32.5c-17.955803,0 -32.5,-14.544197 -32.5,-32.5c0,-17.955801 14.544197,-32.5 32.5,-32.5z"/>
	</svg>

	<!-- checkmark to show on success -->
	<svg class="checkmark" width="70" height="70">
		<path d="m31.5,46.5l15.3,-23.2"/>
		<path d="m31.5,46.5l-8.5,-7.1"/>
	</svg>

	<!-- cross to show on error -->
	<svg class="cross" width="70" height="70">
		<path d="m35,35l-9.3,-9.3"/>
		<path d="m35,35l9.3,9.3"/>
		<path d="m35,35l-9.3,9.3"/>
		<path d="m35,35l9.3,-9.3"/>
	</svg>

</div><!-- /progress-button -->

Используем Method Draw, ведь проще всего воспользоваться онлайн SVG генератором, чтобы нарисовать галочку и крестик для кнопки. Размеры всех SVG будут 70х70, так как высота нашей кнопки 70рх. Если мы хотим круг с бордером толщиной в 5 единиц нам нужно установить правильный радиус, когда будим рисовать его в графическом редакторе, да так чтобы весь круг с его бордером имел высоту 70рх. Заметьте, что в SVG обводка рисуется симметрично границе объекта. К примеру, обводка толщиной в 2рх увеличит круг радиусом в 10рх к реальным толщине и высоте 20+2 вместо 20+4(ширина бордера дважды), то есть формула 2*r+border. Для нашего случая мы знаем 2*r+5=70, от сюда наш круг должен иметь радиус в 32,5рх. Таким образом выходит: />.

К сожалению, мы не можем использовать только эти базовые размеры, потому что дефолтные параметры вставки у каждого браузера свои и мы не можем контролировать, где «анимация загрузки начнётся». Таким образом, нам придётся конвертировать эту форму в path и использовать его. Вы можете легко осуществить это в Method Draw under Object > Convert to Path.

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

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

CSS


Сперва необходимо стилизовать контейнер для кнопки:

.progress-button {
	position: relative;
	display: inline-block;
	text-align: center;
}

Укажем нашей кнопке цвета и шрифты. Что бы она соответствовала концепту, установим правильный бордер и шрифт Montserrat:

.progress-button button {
	display: block;
	margin: 0 auto;
	padding: 0;
	width: 250px;
	height: 70px;
	border: 2px solid #1ECD97;
	border-radius: 40px;
	background: transparent;
	color: #1ECD97;
	letter-spacing: 1px;
	font-size: 18px;
	font-family: 'Montserrat', sans-serif;
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s, border-width 0.3s, border-color 0.3s;
	transition: background-color 0.3s, color 0.3s, width 0.3s, border-width 0.3s, border-color 0.3s;
}

Также ставим transition для всех свойств, которые будут анимироваться (background-color, width etc.).

При наведении курсора мыши меняем цвет фона и цвет текста:

.progress-button button:hover {
	background-color: #1ECD97;
	color: #fff;
}

Уберем все обводки (outline):

.progress-button button:focus {
	outline: none;
}

Все SVG должны быть находиться по центру, все pointer-events отключены:

.progress-button svg {
	position: absolute;
	top: 0;
	left: 50%;
	-webkit-transform: translateX(-50%);
	transform: translateX(-50%);
	pointer-events: none;
}

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

.progress-button svg path {
	opacity: 0;
	fill: none;
}

Наш загрузочный круг будет создан установкой stroke-width в 5 единиц:

.progress-button svg.progress-circle path {
	stroke: #1ECD97;
	stroke-width: 5;
}

Success/fail индикаторы будут иметь обводку тоньше, и она должна быть белой. Свойству stroke-linecap установим значение round, так они будут красивее. У них установим быстрое изменение прозрачности:

.progress-button svg.checkmark path,
.progress-button svg.cross path {
	stroke: #fff;
	stroke-linecap: round;
	stroke-width: 4;
	-webkit-transition: opacity 0.1s;
	transition: opacity 0.1s;
}

Теперь давайте подрезюмируем и вспомним наш master plan. Нам необходимо было иметь возможность стилизовать три добавленных состояния (помимо дефолтного) кнопки и специальных элементов. Будем использовать классы “loading”, “success” и “error” для их индикации.

Кнопка станет круглой и будет выглядеть в точности как загрузочный круг SVG, когда мы начнем процесс загрузки:
.loading.progress-button button {
	width: 70px; /* make a circle */
	border-width: 5px;
	border-color: #ddd;
	background-color: transparent;
	color: #fff;
}

Помним, что мы уже установили transition, когда задавали стили для кнопки. Текст должен быстро исчезнуть, когда начнется анимация загрузки,…

.loading.progress-button span {
	-webkit-transition: opacity 0.15s;
	transition: opacity 0.15s;
}

… путем установки нулевого значения для прозрачности:

.loading.progress-button span,
.success.progress-button span,
.error.progress-button span {
	opacity: 0; /* keep it hidden in all states */
}

При смене состояний с loading на success/error нам не нужен transition, просто оставим текст скрытым.

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

/* Transition for when returning to default state */
.progress-button button span {
	-webkit-transition: opacity 0.3s 0.1s;
	transition: opacity 0.3s 0.1s;
}

При достижении последнего состояния необходимо переопределить transition, так как нам не надо анимировать цвет бордера или широту кнопки:

.success.progress-button button,
.error.progress-button button {
	-webkit-transition: background-color 0.3s, width 0.3s, border-width 0.3s;
	transition: background-color 0.3s, width 0.3s, border-width 0.3s;
}

Зададим цвета для последнего состояния:

.success.progress-button button {
	border-color: #1ECD97;
	background-color: #1ECD97;
}

.error.progress-button button {
	border-color: #FB797E;
	background-color: #FB797E;
}

Когда будет применен необходимый класс, нужно показать нашу SVG и анимировать stroke-dashoffset установкой следующео значения для transition:

.loading.progress-button svg.progress-circle path,
.success.progress-button svg.checkmark path,
.error.progress-button svg.cross path {
	opacity: 1;
	-webkit-transition: stroke-dashoffset 0.3s;
	transition: stroke-dashoffset 0.3s;
}

Добавим easing для анимации широты кнопки:

.elastic.progress-button button {
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.25, 0.25, 0.4, 1), border-width 0.3s, border-color 0.3s;
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.25, 0.25, 0.4, 1.6), border-width 0.3s, border-color 0.3s;
	transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.25, 0.25, 0.4, 1.6), border-width 0.3s, border-color 0.3s;
}

.loading.elastic.progress-button button {
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.6, 0, 0.75, 0.75), border-width 0.3s, border-color 0.3s;
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.6, -0.6, 0.75, 0.75), border-width 0.3s, border-color 0.3s;
	transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.6, -0.6, 0.75, 0.75), border-width 0.3s, border-color 0.3s;
}

Если вы любите играть с другими easing-функциями – пробуйте Ceaser, CSS Easing Animation Tool by Matthew Lein.

Со стилями определились, творим магию дальше.

JavaScript



Начнем с инициализации некоторых элементов: button – это html-элемент button, progressEl – SVG элемент кольцо progress bar, а successEl, errorEl – SVG-элементы галочка и крестик соответственно:

function UIProgressButton( el, options ) {
	this.el = el;
	this.options = extend( {}, this.options );
	extend( this.options, options );
	this._init();
}

UIProgressButton.prototype._init = function() {
	this.button = this.el.querySelector( 'button' );
	this.progressEl = new SVGEl( this.el.querySelector( 'svg.progress-circle' ) );
	this.successEl = new SVGEl( this.el.querySelector( 'svg.checkmark' ) );
	this.errorEl = new SVGEl( this.el.querySelector( 'svg.cross' ) );
	// init events
	this._initEvents();
	// enable button
	this._enable();
}

Добавили функцию SVGEI, которая будет использоваться для того, то бы предоставить SVG-элементы и их paths. Мы кэшируем path и соответственно длину для каждого. Изначально мы «оттягиваем» все paths, управляя значениями свойств strokeDasharray и strokeDashoffset. Позже мы «втянем» их обратно, когда покажем загрузочный круг, или галочку, или крестик. Эту технику хорошо объясняет Jake Archibald в статье Animated line drawing in SVG. Устанавливаем значение stroke-dasharray равное длине path и оттягиваем его. Установив значение stroke-dashoffset также равное его длине, мы больше не видим его. Когда нам нужно будет показать фигуру — установим offset на 0, имитируя рисование фигуры:
function SVGEl( el ) {
	this.el = el;
	// the path elements
	this.paths = [].slice.call( this.el.querySelectorAll( 'path' ) );
	// we will save both paths and its lengths in arrays
	this.pathsArr = new Array();
	this.lengthsArr = new Array();
	this._init();
}

SVGEl.prototype._init = function() {
	var self = this;
	this.paths.forEach( function( path, i ) {
		self.pathsArr[i] = path;
		path.style.strokeDasharray = self.lengthsArr[i] = path.getTotalLength();
	} );
	// undraw stroke
	this.draw(0);
}

// val in [0,1] : 0 - no stroke is visible, 1 - stroke is visible
SVGEl.prototype.draw = function( val ) {
	for( var i = 0, len = this.pathsArr.length; i < len; ++i ){
		this.pathsArr[ i ].style.strokeDashoffset = this.lengthsArr[ i ] * ( 1 - val );
	}
}

Далее, мы должны инициализировать onclick event для нашей кнопки. Сначала кнопка превратится в круг (с помощью добавления класса loading). После окончания анимации, либо будет вызвана callback функция, либо прогресс установится на 100%. В данный момент кнопка отключается (это событие должно бы быть самым первым, однако такой браузер как firefox, например, не сможет удалить transitionend event):

UIProgressButton.prototype._initEvents = function() {
	var self = this;
	this.button.addEventListener( 'click', function() { self._submit(); } );
}

UIProgressButton.prototype._submit = function() {
	classie.addClass( this.el, 'loading' );
	
	var self = this,
		onEndBtnTransitionFn = function( ev ) {
			if( support.transitions ) {
				this.removeEventListener( transEndEventName, onEndBtnTransitionFn );
			}
			
			this.setAttribute( 'disabled', '' );

			if( typeof self.options.callback === 'function' ) {
				self.options.callback( self );
			}
			else {
				self.setProgress(1);
				self.stop();
			}
		};

	if( support.transitions ) {
		this.button.addEventListener( transEndEventName, onEndBtnTransitionFn );
	}
	else {
		onEndBtnTransitionFn();
	}
}

Как только прогресс достигнет 100%, необходимо обновить заполнение загрузочного круга. Затем нужно показать либо галочку, либо крестик. Через некоторое время (options.statusTime) мы уберём все индикаторы состояния и заново включаем кнопку. Обратите внимание, что все переходы контролируются посредством CSS.
UIProgressButton.prototype.stop = function( status ) {
	var self = this,
		endLoading = function() {
			self.progressEl.draw(0);
			
			if( typeof status === 'number' ) {
				var statusClass = status >= 0 ? 'success' : 'error',
					statusEl = status >=0 ? self.successEl : self.errorEl;

				statusEl.draw( 1 );
				// add respective class to the element
				classie.addClass( self.el, statusClass );
				// after options.statusTime remove status and undraw the respective stroke and enable the button
				setTimeout( function() {
					classie.remove( self.el, statusClass );
					statusEl.draw(0);
					self._enable();
				}, self.options.statusTime );
			}
			else {
				self._enable();
			}

			classie.removeClass( self.el, 'loading' );
		};

	// give it a little time (ideally the same like the transition time) so that the last progress increment animation is still visible.
	setTimeout( endLoading, 300 );
}

Кнопка готова!

«We hope you enjoyed this tutorial and find it useful!»

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


  1. namespace
    28.11.2015 12:31
    +16

    Какой же отвратительный перевод!


    1. SerDIDG
      29.11.2015 05:59
      +6

      Как и JS код )


      1. mapron
        29.11.2015 14:52
        +2

        Простите, я не понимаю, зачем здесь столько JS кода? Несклько gif взамен не были бы лучше? (с точки зрения и веса, и производительности). Может я что-то не понял в новомодных трендах?


        1. mannaro
          29.11.2015 15:46
          +1

          Я думаю, что тут используется SVG+JS для легкой кастомизации кнопки (нужно будет сменить цвет на красный, например). Да и SVG на retina выглядит гораздо лучше (знаю, можно использовать @2x, но тут-то все из коробки).
          Но вот качество конкретно этого JS страдает, это да.


        1. Andchir
          29.11.2015 15:47

          Кода не так уж и много. Как вы с помощью GIF сделаете показ процесса загрузки в процентах?


  1. kahi4
    29.11.2015 16:29
    +17

    Выглядит красиво. Но хватит отъедать впустую ресурсы моего компьютера! В 99.9% вы не сможете ничего адекватного выводить в виде процентов, а без них все делается в 10 строчек css: накидано за 20 секунд. В моем примере весь js только классы переключает.
    Ни svg, ни сотни вложенных элементов, простой и чистый стиль (правда, не без своих недостатков).

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


    1. mapron
      30.11.2015 05:37

      Мне гораздо больше нравится. Даже не внешне, а с точки зрения понимания и поддержки кода. Открыл — и мне, даже нифига не фронтенд-разработчику, усе ясно.


    1. taliban
      01.12.2015 14:01

      Супер, гораздо предпочтительней того что в тстатье описано, но есть одно но:

      .button.load {
          background-color: transparent;
      }
      

      Иначе при анимации и наведении мыши фон меняется и смотрится это все не ахти