Bài viết này sẽ hướng dẫn bạn cách tạo hiệu ứng load trang như trên Fontface Ninja bằng việc sử dụng kết hợp CSS, SVD và Javascript.

Như chúng ta đã biết, hiệu ứng load trang là một cách làm sáng tạo để thu hút người truy cập trong khi chờ đợi. Ý tưởng trong bài viết này dựa trên hiệu ứng load trang rất đẹp mắt trên website Fontface.ninja.

Trước tiên, bạn nên vào website Fontface.ninja để quan sát hiệu ứng. Và bây giờ chúng tôi sẽ tái tạo lại hiệu ứng mà bạn vừa nhìn thấy trên Fontface.ninja cùng một số điều chỉnh và một bản demo thứ hai với những hiệu ứng khác nhau.

Trang loading này ta sẽ đặt cho nó class là ip-header, bao gồm logo của website là ip-logo và một đường tròn loader là ip-loader. Tất cả mọi thứ đó sẽ đặt gói gọn trong 1 container để dễ quản lý.

Chúng ta sẽ sử dụng SVG cho logo mà không sử dụng hình ảnh thông thường trong thẻ <img>, bởi vì phần animation cần đổi màu logo và việc này SVG hỗ trợ tốt hơn so với <img>. Ngoài ra, chúng ta cũng sử dụng SVG cho đường tròn loader để tạo kiểu đường đi cho CSS. Tiếp theo là tạo một chút script cho animation của SVG loading và sau đó kiểm soát animation của trang với class mà chúng ta thêm vào container chính.

Main content có class ip-main và sau đó ta sẽ tạo animation cho thư mục con, tiêu đề, các bộ phận và box bên trong.

Như vậy ta sẽ có đoạn HTML cho trang loading như sau:

<div id="ip-container" class="ip-container">

	<!-- initial header -->
	<header class="ip-header">

		<h1 class="ip-logo">
			<svg class="ip-inner" width="100%" height="100%" viewBox="0 0 300 160" preserveAspectRatio="xMidYMin meet" aria-labelledby="logo_title">
				<title id="logo_title">Delightful Demonstrations by Codrops</title>
				<path d="...our super-long path..." />
			</svg>
		</h1>

		<div class="ip-loader">
			<svg class="ip-inner" width="60px" height="60px" viewBox="0 0 80 80">
				<path class="ip-loader-circlebg" d="M40,10C57.351,10,71,23.649,71,40.5S57.351,71,40.5,71 S10,57.351,10,40.5S23.649,10,40.5,10z"/>
				<path id="ip-loader-circle" class="ip-loader-circle" d="M40,10C57.351,10,71,23.649,71,40.5S57.351,71,40.5,71 S10,57.351,10,40.5S23.649,10,40.5,10z"/>
			</svg>
		</div>

	</header>

	<!-- main content -->
	<div class="ip-main">

		<h2>Make yourself at home.</h2>

		<div class="browser clearfix">
			<div class="box">
				<span class="icon-bell"></span>
				<p>...</p>
			</div>
			<div class="box">
				<span class="icon-heart"></span>
				<p>...</p>
			</div>
			<div class="box">
				<span class="icon-cog"></span>
				<p>...</p>
			</div>
		</div>
		
	</div>
</div><!-- /container -->

CSS

Lưu ý rằng CSS không chứa bất kì nhà cung cấp nào trước đây nhưng chúng ta sẽ tìm thấy chúng trong các file.

Trước hết chúng ta cần một số phông chữ cần thiết cho các đoạn văn bản và các icon trong box. Các icon được sử dụng trong demo là từ bộ “Feather icon set” và chúng ta vừa mới tạo font icon bằng “Icomoon App”. Phông hình nộm (dummy) bạn có thể tạo ở “Blokk”, ở đó bạn có thể tìm ra những font chữ thực sự tuyệt vời khi tạo wireframe và mockup.

@font-face {
	font-weight: normal;
	font-style: normal;
	font-family: 'Blokk';
	src: url('../fonts/blokk/BLOKKRegular.eot');
	src: url('../fonts/blokk/BLOKKRegular.eot?#iefix') format('embedded-opentype'),
		 url('../fonts/blokk/BLOKKRegular.woff') format('woff'),
		 url('../fonts/blokk/BLOKKRegular.svg#BLOKKRegular') format('svg');
}

@font-face {
	font-weight: normal;
	font-style: normal;
	font-family: 'feather';
	src:url('../fonts/feather/feather.eot?-9jv4cc');
	src:url('../fonts/feather/feather.eot?#iefix-9jv4cc') format('embedded-opentype'),
		url('../fonts/feather/feather.woff?-9jv4cc') format('woff'),
		url('../fonts/feather/feather.ttf?-9jv4cc') format('truetype'),
		url('../fonts/feather/feather.svg?-9jv4cc#feather') format('svg');
}

Nếu bạn muốn phần header lấp đầy toàn bộ viewport thì hãy để 100% chiều dài, 100% chiều rộng và set vị trí của nó cố định như sau:

.ip-header {
	position: fixed;
	top: 0;
	z-index: 100;
	min-height: 480px;
	width: 100%;
	height: 100%;
	background: #f1f1f1;
}

Bây giờ chúng ta sẽ gỡ bỏ tất cả các margin từ logo tiêu đề:

.ip-header h1 {
	margin: 0;
}

Cả phần logo và loader sẽ được đặt vị trí absolute và bạn kéo dài chúng trong viewport như sau:

.ip-logo,
.ip-loader {
	position: absolute;
	left: 0;
	width: 100%;
	opacity: 0;
	cursor: default;
	pointer-events: none;
}

Thay vì chỉ đơn giản sử dụng logo và đặt vị trí nó ở trung tâm của header, chúng ta có thể làm việc với một giá trị cụ thể: chiều cao viewport. Vì vậy, chỉ cần đặt logo đến 100% chiều cao và dịch nó 25% thì logo SVG sẽ nằm ở giữa trang:

.ip-logo {
	top: 0;
	height: 100%;
	transform: translate3d(0,25%,0);
}

Chúng ta đặt vị trí logo ở đáy viewport:

.ip-loader {
	bottom: 20%;
}

Các SVG mà chúng ta đã đưa class ip-inner sẽ được hiển thị như block và chúng ta chỉnh về trung tâm theo chiều ngang với margin tự động:

.ip-header .ip-inner {
	display: block;
	margin: 0 auto;
}

Logo SVG không nên quá lớn hoặc quá nhỏ, vì vậy, chúng ta có thể thiết lập chiều rộng tối đa và tối thiểu.

.ip-header .ip-logo svg {
	min-width: 320px;
	max-width: 480px;
	width: 25%;
}

Sau khi thêm đường logo SVG, bạn có thể trực tiếp tạo kiểu dáng màu sắc cho nó:

.ip-header .ip-logo svg path {
	fill: #ef6e7e;
}

Cùng vị trí với loader:

.ip-header .ip-loader svg path {
	fill: none;
	stroke-width: 6;
}

Đường đầu tiên lấp đầy bằng màu xám:

.ip-header .ip-loader svg path.ip-loader-circlebg {
	stroke: #ddd;
}

Và đường thứ hai sẽ có transition tiến bộ nếu chúng ta kiểm soát trong JS. Nhưng ở đây, bạn phải xác định các transition của stroke-dashoffset:

.ip-header .ip-loader svg path.ip-loader-circle {
	transition: stroke-dashoffset 0.2s;
	stroke: #ef6e7e;
}

Và bây giờ, chúng ta sẽ tạo kiểu dáng cho nội dung trang trong phần ip-main:

.ip-main {
	overflow: hidden;
	margin: 0 auto;
	padding: 160px 0 10em 0;
	max-width: 1100px;
	width: 90%;
}

Để nó tự phản ứng, tiêu đề chúng ta sẽ đặt là vw:

.ip-main h2 {
	margin: 0;
	padding: 0.5em 0 1em;
	color: #be4856;
	text-align: center;
	font-size: 4.25em;
	font-size: 4vw;
	line-height: 1;
}

Hãy thêm một browser image:

.browser {
	margin: 0 auto;
	padding-top: 8%;
	min-height: 400px;
	max-width: 1000px;
	width: 100%;
	border-radius: 8px;
	background: #fff url(../img/browser.png) no-repeat 50% 0;
	background-size: 100%;
	color: #d3d3d3;
}

Và thêm hộp hình nộm (dummy box):

.box {
	float: left;
	padding: 3.5em;
	width: 33.3%;
	font-size: 0.7em;
	line-height: 1.5;
}

.box p {
	font-family: 'Blokk', Arial, sans-serif;
}

Mỗi hộp sẽ có 1 icon:

[class^="icon-"]::before, 
[class*=" icon-"]::before {
	display: block;
	margin-bottom: 0.5em;
	padding: 0.5em;
	border-radius: 5px;
	background: #dfdfdf;
	color: #fff;
	text-align: center;
	text-transform: none;
	font-weight: normal;
	font-style: normal;
	font-variant: normal;
	font-size: 5em;
	font-family: 'feather';
	line-height: 1;
	speak: none;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.icon-bell:before {
	content: "\e006";
}

.icon-cog:before {
	content: "\e023";
}

.icon-heart:before {
	content: "\e024";
}

Bây giờ chúng ta phải xác định animation. Như đã được đề cập lúc ban đầu, chúng ta sẽ kiểm soát animation bằng việc thêm class cho container chính (main container).

Animation đầu tiên của các header sẽ làm chúng di chuyển xuống phía dưới:

.loading .ip-logo,
.loading .ip-loader {
	opacity: 1;
	animation: animInitialHeader 1s cubic-bezier(0.7,0,0.3,1) both;
}

.loading .ip-loader {
	animation-delay: 0.2s;
}

@keyframes animInitialHeader {
	from { 
		opacity: 0; 
		transform: translate3d(0,800px,0); 
	}
}

Việc loading sẽ cần một chút trì hoãn trước khi kết thúc. Lưu ý rằng, chúng ta sẽ tạo animation cho đường tròn với JS. Vì vậy chúng ta cần thêm một “state” khác để tắt khi phần animation chạy xong. Bạn chỉ cần đưa class loaded vào container và áp dụng những animation sau:

.loaded .ip-logo,
.loaded .ip-loader {
	opacity: 1;
}

.loaded .ip-logo {
	transform-origin: 50% 0;
	animation: animLoadedLogo 1s cubic-bezier(0.7,0,0.3,1) forwards;
}

@keyframes animLoadedLogo {
	to { 
		transform: translate3d(0,100%,0) translate3d(0,50px,0) scale3d(0.65,0.65,1); 
	}
}

.loaded .ip-logo svg path {
	transition: all 0.5s ease 0.3s;
	fill: #fff;
}

.loaded .ip-loader {
	animation: animLoadedLoader 0.5s cubic-bezier(0.7,0,0.3,1) forwards;
}

@keyframes animLoadedLoader {
	to { 
		opacity: 0; 
		transform: translate3d(0,-100%,0) scale3d(0.3,0.3,1); 
	}
}

Logo di chuyển xuống 100% (nhớ rằng logo của chúng ta là 100% chiều cao viewport, vì vậy nó sẽ di chuyển toàn bộ chiều cao của màn hình). Màu sắc của đường SVG sẽ thay đổi với một transition.

Header cố định cũng cần phải di chuyển lên trên:

.loaded .ip-header {
	animation: animLoadedHeader 1s cubic-bezier(0.7,0,0.3,1) forwards;
}

@keyframes animLoadedHeader {
	to { transform: translate3d(0,-100%,0); }
}

Hãy để ý phần content. Ở đây, bạn có thể tạo ra rất nhiều hiệu ứng sáng tạo. Tất nhiên việc này phụ thuộc vào trang của bạn nhưng trong trường hợp này, chúng tôi muốn làm mờ một chút trong khi nó di chuyển từ dưới lên trên:

/* Content animations */
.loaded .ip-main h2,
.loaded .ip-main .browser,
.loaded .ip-main .browser .box,
.loaded .codrops-demos {
	animation: animLoadedContent 1s cubic-bezier(0.7,0,0.3,1) both;
}

.loaded .ip-main .browser,
.loaded .ip-main .browser .box:first-child {
	animation-delay: 0.1s;
}

.loaded .ip-main .browser .box:nth-child(2) {
	animation-delay: 0.15s;
}

.loaded .ip-main .browser .box:nth-child(3) {
	animation-delay: 0.2s;
}

@keyframes animLoadedContent {
	from { 
		opacity: 0; 
		transform: translate3d(0,200px,0); 
	}
}

Để các box trong bộ phận trình duyệt trì hoãn một chút sẽ tạo ra thêm một hiệu ứng tiện lợi.

Để tránh các vấn đề di chuyển và những khoảng trống ở cuối trang, bạn cần phải chuyển đổi vị trí của các tiêu đề từ fixed thành absolute. Chúng ta có thể kiểm soát bằng việc thêm một class vào phần body (hoặc bất cứ parent nào) ngay khi tất cả animation đã hoàn thành. Như vậy, bạn có thể di chuyển vị trí:

.layout-switch .ip-header {
	position: absolute;
}

Nếu bạn không có JavaScript, chúng tôi có thể chỉ cho bạn sau tất cả animation. Bạn có thể thiết lập header với sự di chuyển và kích thước logo tương đối:

.no-js .ip-header {
	position: relative;
	min-height: 0px;
}

.no-js .ip-header .ip-logo {
	margin-top: 20px;
	height: 180px;
	opacity: 1;
	transform: none;
}

.no-js .ip-header .ip-logo svg path {
	fill: #fff;
}

Cuối cùng, chúng ta phải quan tâm đến phần headline lớn và những box cho màn hình nhỏ hơn:

@media screen and (max-width: 45em) {

	.ip-main h2 {
		font-size: 2.25em;
		font-size: 10vw;
	}

	.box {
		width: 100%%;
	}

}

 

The JavaScript

JavaScript được chia ra làm hai phần, pathLoader.js để điều khiển việc giả lập loading của đường tròn loader và main.js để điều khiển việc khởi tạo cũng như áp dụng animation vào các phần tử HTML.

Để giả lập giai đoạn loading ban đầu, chúng ta sẽ thay đổi thuộc tính stroke-dashoffset của đường tròn loading đến khi stroke-dashoffset = 0 tức là đã kết thúc việc giả lập. Chúng ta sẽ làm như sau:

function PathLoader( el ) {
	this.el = el;
	// clear stroke
	this.el.style.strokeDasharray = this.el.style.strokeDashoffset = this.el.getTotalLength();
}

PathLoader.prototype._draw = function( val ) {
	this.el.style.strokeDashoffset = this.el.getTotalLength() * ( 1 - val );
}

PathLoader.prototype.setProgress = function( val, callback ) {
	this._draw(val);
	if( callback && typeof callback === 'function' ) {
		// give it a time (ideally the same like the transition time) so that the last progress increment animation is still visible.
		setTimeout( callback, 200 );
	}
}

PathLoader.prototype.setProgressFn = function( fn ) {
	if( typeof fn === 'function' ) { fn( this ); }
}

Chúng ta sẽ tương tác với PathLoader bằng hàm setProgressFn với thông số truyền vào cũng là 1 hàm, ta đặt simulationFn:

var simulationFn = function(instance) {
	var progress = 0,
		interval = setInterval( function() {
			progress = Math.min( progress + Math.random() * 0.1, 1 );
			instance.setProgress( progress );
			// reached the end
			if( progress === 1 ) {
				clearInterval( interval );
			}
		}, 100 );
};

var loader = new PathLoader([pathselector]);
loader.setProgressFn(simulationFn);

Tiếp theo, bạn có thể tạo ra phần script còn lại của chúng ta trong main.js. Đầu tiên chúng ta khởi tạo và khai báo biến:

var support = { animations : Modernizr.cssanimations },
	container = document.getElementById( 'ip-container' ),
	header = container.querySelector( 'header.ip-header' ),
	loader = new PathLoader( document.getElementById( 'ip-loader-circle' ) ),
	animEndEventNames = { 'WebkitAnimation' : 'webkitAnimationEnd', 'OAnimation' : 'oAnimationEnd', 'msAnimation' : 'MSAnimationEnd', 'animation' : 'animationend' },
	// animation end event name
	animEndEventName = animEndEventNames[ Modernizr.prefixed( 'animation' ) ];

Chúng ta bắt đầu animation (cả logo và loader) bằng việc thêm class loading cho container chính. Sau khi animation kết thúc, chúng ta bắt đầu “fake” animation loading trên loader SVG. Lưu ý rằng trong khi những animation đang chạy, chúng ta không cho phép bất cứ trang nào cuộn:

function init() {
	var onEndInitialAnimation = function() {
		if( support.animations ) {
			this.removeEventListener( animEndEventName, onEndInitialAnimation );
		}

		startLoading();
	};

	// disable scrolling
	window.addEventListener( 'scroll', noscroll );

	// initial animation
	classie.add( container, 'loading' );

	if( support.animations ) {
		container.addEventListener( animEndEventName, onEndInitialAnimation );
	}
	else {
		onEndInitialAnimation();
	}
}

// no scroll
function noscroll() {
	window.scrollTo( 0, 0 );
}

Một lần nữa, chúng ta sẽ thấy hài lòng khi một thứ gì đó được tải bằng việc thông qua một chức năng custom cho setProgressFn. Ngay khi animation kết thúc, bạn phải thay thế class loading bằng class loaded bắt đầu với animation chính cho header và content. Sau khi hoàn thành, bạn thêm class layout-switch vào body và cho phép cuộn:

function startLoading() {
	// simulate loading something..
	var simulationFn = function(instance) {
		var progress = 0,
			interval = setInterval( function() {
				progress = Math.min( progress + Math.random() * 0.1, 1 );

				instance.setProgress( progress );

				// reached the end
				if( progress === 1 ) {
					classie.remove( container, 'loading' );
					classie.add( container, 'loaded' );
					clearInterval( interval );

					var onEndHeaderAnimation = function(ev) {
						if( support.animations ) {
							if( ev.target !== header ) return;
							this.removeEventListener( animEndEventName, onEndHeaderAnimation );
						}

						classie.add( document.body, 'layout-switch' );
						window.removeEventListener( 'scroll', noscroll );
					};

					if( support.animations ) {
						header.addEventListener( animEndEventName, onEndHeaderAnimation );
					}
					else {
						onEndHeaderAnimation();
					}
				}
			}, 80 );
	};

	loader.setProgressFn( simulationFn );
}

Và như vậy chúng ta đã xong tất cả mọi thứ. Chúng tôi hi vọng bạn sẽ thích phần hướng dẫn này và thấy nó giúp ích, đồng thời tạo cảm hứng sáng tạo cho bạn.