This Website

I've learned a lot by building this website, web is very different from other technologies I have learned before. I was mostly interested in mobile development, and I was very against web. However, after I started learning web development, I found most of my previous ideas about web are either not true, or have workarounds. Now I like web development, it became one of my favorite technology.

This website is written with HTML, CSS and Javascript. For mock-up and image processing, I used Adobe Photoshop and Illustrator. For IDEs I chose Sublime Text and Brackets, I highly recommend them for web development.

I. UI/UX

I use responsive and interactive design for this website, so viewers would have a good experience on different screen sizes and devices. To achieve that, there are 4 CSS files for different screen size layouts, another 4 CSS files for loading specific size of images depending on your device (to load the page faster and save your cellular data), also a Javascript file for interactions.

The UI is built from scratch, no third party UI library or framework used. Most animations are written with JQuery animation, and I use CSS animation as well, mainly because of JQuery animation's well-known performance issue and lack of functionality (e.g. animate color change). Some of the navigation bar animation you see at top are written with CSS.

JQuery vs. CSS

If you hover the cursor over the project introductions on the home page, some animations will happen, and when your mouse leaves, all animations stop and reverse back. The code below is responsible for the later part. However, the color animation is handled by CSS.


	// JQuery Animation
	var $prjContainers = $('section.project div.prjContainer');
	$prjContainers.on('mouseleave blur', function(e) {
			if (windowSize === 'large' || windowSize === 'medium') {
				e.stopPropagation();
				var $introContainer = $(this).find('div.prjIntroContainer');
				if ($introContainer.hasClass('expanded')) {
					$introContainer.stop(true).animate({
						'top': (prjContainerWidth - PRJ_NAME_HEIGHT) + 'px'
					}, 300, 'linear');
					$introContainer.removeClass('expanded');
				}
			}
	});
				

Next chunk of code defines how to change navigation bar color from white to black, but the timing is controlled by Javascript with some DOM manipulation, you can learn more about CSS animation here.


	/* CSS Animation */
	@keyframes whiteToBlack {
		from {background-color: #fff;}
		to {background-color: rgba(0, 0, 0, .5);}
	}
				

NOTE: Since CSS animation is not officially supported by most browsers yet, real applications need to be implemented with additional "-webkit-", "-moz-", "-ms-", "-o-" prefixes. These prefixes are omitted here for briefness.

Loader

On desktop, images on the Gallery page have a loader while they are loading, as shown below. I am using solid background color instead of loaders to indicate loading image on mobile devices, because of some performance issue.

Loader animation is written with pure CSS, but the first step is to add loaders behind all image containers.


	if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { isMobile = true; }
	// Insert image loader before all image containers. Because of performance issues, only insert loaderContainer and loader on desktop devices.
	if (!isMobile) {
		$('div.img_container').before('
'); } else { // Use background color to indicate there's an image. $('div.img_container').css('background-color', '#EDEDED'); }

As you can see, there are 3 parts of loader: the main container, scale container, and the actual loader itself. Container is basically a play ground for the loader, scale container scales the loader during animation, loader is the actual loader with the spin animation. The reason we need a separate scale container instead of scale loader itself, is that CSS3 animation does not support multiple animations for one element at the same time for now.

Let's implement the layout and animation for loader with CSS.


	.loaderContainer {
		background-color: #fff;
		position: absolute;
		box-sizing: border-box;
	}

	.loaderContainer .scaleContainer {
		position: absolute;
		border-radius: 100%;
		background-color: transparent;
		animation: loaderScale 1s infinite ease-in-out;
	}

	.loaderContainer .scaleContainer .loader {
		background: transparent;
		position: absolute;
		border-radius: 100%;
		border-top: 1em solid rgba(0, 0, 0, 0.4);
		border-right: 1em solid rgba(0, 0, 0, 0.1);
		border-bottom: 1em solid rgba(0, 0, 0, 0.4);
		border-left: 1em solid rgba(0, 0, 0, 0.1);
		animation: loaderSpin 1s infinite ease-in-out;
	}
				

Now we have the basic layout for our loader ready to go, we also defined animation time, length, and style. However, we still need to define what "loaderScale" and "loaderSpin" animations are. That's what we're going to do next.


	@keyframes loaderSpin {
		from { transform: rotate(0deg); }
		to { transform: rotate(360deg); }
	}

	@keyframes loaderScale {
		0% { transform: scale(.75); }
		50% { transform: scale(1); }
		100% { transform: scale(.75); }
	}
				

The spin animation is easy, just need to spin it from 0 degrees to 360 degrees. For scale animation, it's a bit more complex. We want to scale the loader up then down during one iteration of the spin animation, so we have to break it to 2 parts. From 0% to 50%, scale it up; from 50% to 100%, scale it down. Their syntax are slightly different, this is also an example of two ways to use CSS keyframes animation.

II. Third Party Resources

The most heavily used library is JQuery, I'm also using Google Analytics to help me track website traffic, and Google Maps APIs for the Google Map on About page. Below I will use Google Maps APIs as a demonstration, and some design thoughts behind it as well.

The map view behaves differently on mobile and desktop devices. Since mobile devices have limited screen size, bad precision input (fingers), and far less data usage, I decided to load a static map without any interactive controls. However on desktop devices, screens are usually bigger, mouse cursor is more accurate, data usage is not an issue, I decided to load a interactive Google Map. Also I disabled scroll to zoom on the map view, so when users are scrolling the page, they wouldn't accidentally start zooming map, that would be very annoying.

There's a Javascript file google_map.js, almost all map actions are handled there. Below is the code snippet for initializing the map.


	// Google map initialization
	function initializeGoogleMap() {
		var zoomValue = 1;
		if (windowSize === 'large') { zoomValue = 2; };
		var mapOptions = {
			zoom: zoomValue,
			scrollwheel: false,
			draggable: !isMobile,
			disableDoubleClickZoom: isMobile,
			disableDefaultUI: true,
			panControl: false,
			mapTypeId: google.maps.MapTypeId.ROADMAP,
			center: new google.maps.LatLng(39.3,3.87),
			mapTypeControl: !isMobile,
			mapTypeControlOptions: {
				style: google.maps.MapTypeControlStyle.DEFAULT,
				position: google.maps.ControlPosition.LEFT_BOTTOM,
				mapTypeIds: [google.maps.MapTypeId.ROADMAP,google.maps.MapTypeId.SATELLITE]
			},
			zoomControl: !isMobile,
			zoomControlOptions: {
				style: google.maps.ZoomControlStyle.SMALL,
				position: google.maps.ControlPosition.RIGHT_BOTTOM
			},
			streetViewControl: !isMobile,
			streetViewControlOptions: { position: google.maps.ControlPosition.RIGHT_BOTTOM }
		}
		map = new google.maps.Map(document.querySelector('div.travel_map', mapOptions);
	}
				

Also when you scroll the page to the Google Map section, there are drop pins start falling on the map to mark places I've traveled before. That is also done with Google Maps APIs, however, the timing is controlled by using "setTimeout" function.


	function addMarker(position, name) {
		markers.push(new google.maps.Marker({
			position: position,
			map: map,
			title: name,
			animation: google.maps.Animation.DROP
		}));
	}

	function dropMarkers() {
		for (var i = 0; i < travelLocations.length; i++) {
			var loc = travelLocations[i],
					name = locationTitles[i];
			(function(l, n){
				setTimeout(function() {
					addMarker(l, n);
				}, i * 150);
			}(loc, name));
		}
		markersOn = true;
	}