cssAudio - Activefile-genericCSS - ActiveGeneric - ActiveHTML - ActiveImage - ActiveJS - ActiveSVG - ActiveText - Activefile-genericVideo - ActiveLovehtmlicon-new-collectionicon-personicon-teamlog-outoctocatpop-outspinnerstartv

Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

Quick-add: + add another resource

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

Quick-add: + add another resource

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

            
              <div class="globe js-globe">
	<ul class="globe-list js-list"></ul>
	<canvas class="globe-canvas js-canvas"></canvas>
</div>

            
          
!
            
              /* COLOURS */

$colour-cyan: #00FFD3;
$colour-white: #FFFFFF;
$colour-black: #000000;



/* RESETS */

html,
body,
*,
*:before,
*:after {
	margin: 0;
	padding: 0;
	-webkit-box-sizing: border-box;
	-moz-box-sizing: border-box;
	-ms-box-sizing: border-box;
	box-sizing: border-box;
	-webkit-text-size-adjust: 100%;
	-webkit-font-smoothing: antialiased;
}

$colourStart: #8D07C8;
$colourMid: #260F77;
$colourEnd: #030B3A;

body {
	position: relative;
	width: 100vw;
	height: 100vh;
	font-family: "Cairo", sans-serif;
	font-size: 14px;
	line-height: 14px;
	font-weight: 400;
	text-rendering: optimizeLegibility;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	-moz-font-feature-settings: "liga" on;
	color: #FFFFFF;
	overflow-x: hidden;
	background: $colourStart;
	background: -moz-linear-gradient(-45deg, $colourStart 0%, $colourMid 50%, $colourEnd 100%);
	background: -webkit-linear-gradient(-45deg, $colourStart 0%, $colourMid 50%, $colourEnd 100%);
	background: linear-gradient(135deg, $colourStart 0%, $colourMid 50%, $colourEnd 100%);
	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{$colourStart}', endColorstr='${colourEnd}', GradientType=1);
}

@mixin animation ($animation) {
	-webkit-animation: $animation;
	-moz-animation: $animation;
	-ms-animation: $animation;
	-o-animation: $animation;
	animation: $animation;
}

@mixin transition ($transition) {
	-webkit-transition: $transition;
	-moz-transition: $transition;
	-ms-transition: $transition;
	-o-transition: $transition;
	transition: $transition;
}

@mixin transform ($transform) {
	-webkit-transform: $transform;
	-moz-transform: $transform;
	-ms-transform: $transform;
	-o-transform: $transform;
	transform: $transform;
}



/* GLOBE */

.globe {
	position: relative;
	width: 100vw;
	height: 100vh;
}

.globe .globe-list {
	z-index: 10;
	position: absolute;
	left: 0;
	top: 0;
	list-style: none;
}

$dotSize: 16px;
$dotSizeLarge: $dotSize + ($dotSize / 2);

.globe .globe-list {
	opacity: 0;
	@include transition(opacity 3s cubic-bezier(0.175, 0.885, 0.320, 1.275));
}

.globe .globe-list.active {
	opacity: 1;
}

.globe .globe-list > li {
	opacity: 0.4;
	position: absolute;
	margin-left: -($dotSize / 2);
	margin-top: -($dotSize / 2);
	width: $dotSize;
	height: $dotSize;
	border-radius: 50%;
	background: $colour-cyan;
	@include transition(opacity 1s cubic-bezier(0.175, 0.885, 0.320, 1.275));
}

.globe .globe-list > li.active {
	opacity: 1;
	margin-left: -($dotSizeLarge / 2);
	margin-top: -($dotSizeLarge / 2);
	width: $dotSizeLarge;
	height: $dotSizeLarge;
}

.globe .globe-list > li:before {
	content: "";
	opacity: 0.5;
	pointer-events: none;
	position: absolute;
	left: 50%;
	top: 50%;
	margin-left: -($dotSize / 2);
	margin-top: -($dotSize / 2);
	width: $dotSize;
	height: $dotSize;
	border-radius: 50%;
	background: $colour-cyan;
	@include animation(2s pulse infinite linear);
}

@-webkit-keyframes pulse {
	0% { -webkit-transform: scale(1); }
	50% { opacity: 0.5; }
	100% { opacity: 0; -webkit-transform: scale(2); }
}

@-moz-keyframes pulse {
	0% { -moz-transform: scale(1); }
	50% { opacity: 0.5; }
	100% { opacity: 0; -moz-transform: scale(2); }
}

@-ms-keyframes pulse {
	0% { -ms-transform: scale(1); }
	50% { opacity: 0.5; }
	100% { opacity: 0; -ms-transform: scale(2); }
}

@-o-keyframes pulse {
	0% { -o-transform: scale(1); }
	50% { opacity: 0.5; }
	100% { opacity: 0; -o-transform: scale(2); }
}

@keyframes pulse {
	0% { transform: scale(1); }
	50% { opacity: 0.5; }
	100% { opacity: 0; transform: scale(2); }
}

.globe .globe-list > li.active:before {
	margin-left: -($dotSizeLarge / 2);
	margin-top: -($dotSizeLarge / 2);
	width: $dotSizeLarge;
	height: $dotSizeLarge;
}

.globe .globe-list > li.active,
.globe .globe-list > li.active:before {
	background: $colour-white;
}

.globe .globe-list .text {
	position: absolute;
	opacity: 0.8;
	right: $dotSize + 5px;
	top: 50%;
	display: block;
	font-size: 14px;
	line-height: 14px;
	font-weight: 600;
	text-align: right;
	text-shadow: -1px -1px 0 $colour-black, 1px -1px 0 $colour-black, -1px 1px 0 $colour-black, 1px 1px 0 $colour-black;
	color: $colour-white;
	white-space: nowrap;
	@include transform(translateY(-50%));
}

.globe .globe-list > li.active .text {
	opacity: 1;
	right: $dotSizeLarge + 5px;
	font-size: 20px;
	line-height: 20px;
	font-weight: 700;
}

.globe .globe-canvas {
	z-index: 0;
	position: absolute;
	left: 0;
	top: 0;
}

            
          
!
            
              /* VARIABLES */

var canvas, scene, renderer, data;

// Cache DOM selectors
var container = document.getElementsByClassName('js-globe')[0];

// Object for country HTML elements and variables
var elements = {};

// Three group objects
var groups = {
	main: null, // A group containing everything
	globe: null, // A group containing the globe sphere (and globe dots)
	globeDots: null, // A group containing the globe dots
	lines: null, // A group containing the lines between each country
	lineDots: null // A group containing the line dots
};

// Map properties for creation and rendering
var props = {
	mapSize: {
		// Size of the map from the intial source image (on which the dots are positioned on)
		width: 2048 / 2,
		height: 1024 / 2
	},
	globeRadius: 200, // Radius of the globe (used for many calculations)
	dotsAmount: 20, // Amount of dots to generate and animate randomly across the lines
	startingCountry: 'hongkong', // The key of the country to rotate the camera to during the introduction animation (and which country to start the cycle at)
	colours: {
		// Cache the colours
		globeDots: 'rgb(61, 137, 164)', // No need to use the Three constructor as this value is used for the HTML canvas drawing 'fillStyle' property
		lines: new THREE.Color('#18FFFF'),
		lineDots: new THREE.Color('#18FFFF')
	},
	alphas: {
		// Transparent values of materials
		globe: 0.4,
		lines: 0.5
	}
};

// Angles used for animating the camera
var camera = {
	object: null, // Three object of the camera
	controls: null, // Three object of the orbital controls
	angles: {
		// Object of the camera angles for animating
		current: {
			azimuthal: null,
			polar: null
		},
		target: {
			azimuthal: null,
			polar: null
		}
	}
};

// Booleans and values for animations
var animations = {
	finishedIntro: false, // Boolean of when the intro animations have finished
	dots: {
		current: 0, // Animation frames of the globe dots introduction animation
		total: 170, // Total frames (duration) of the globe dots introduction animation,
		points: [] // Array to clone the globe dots coordinates to
	},
	globe: {
		current: 0, // Animation frames of the globe introduction animation
		total: 80, // Total frames (duration) of the globe introduction animation,
	},
	countries: {
		active: false, // Boolean if the country elements have been added and made active
		animating: false, // Boolean if the countries are currently being animated
		current: 0, // Animation frames of country elements introduction animation
		total: 120, // Total frames (duration) of the country elements introduction animation
		selected: null, // Three group object of the currently selected country
		index: null, // Index of the country in the data array
		timeout: null, // Timeout object for cycling to the next country
		initialDuration: 5000, // Initial timeout duration before starting the country cycle
		duration: 2000 // Timeout duration between cycling to the next country
	}
};

// Boolean to enable or disable rendering when window is in or out of focus
var isHidden = false;



/* SETUP */

function getData() {

	var request = new XMLHttpRequest();
	request.open('GET', 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/617753/globe-points.json', true);

	request.onload = function() {
		if (request.status >= 200 && request.status < 400) {
			data = JSON.parse(request.responseText);
			setupScene();
		}
		else {
			showFallback();
		}
	};

	request.onerror = showFallback;

	request.send();

}

function showFallback() {

	/*
		This function will display an alert if WebGL is not supported.
	*/

	alert('WebGL not supported. Please use a browser that supports WebGL.');

}

function setupScene() {

	canvas = container.getElementsByClassName('js-canvas')[0];

	scene = new THREE.Scene();
	renderer = new THREE.WebGLRenderer({
		canvas: canvas,
		antialias: true,
		alpha: true,
		shadowMapEnabled: false
	});
	renderer.setSize(canvas.clientWidth, canvas.clientHeight);
	renderer.setPixelRatio(1);
	renderer.setClearColor(0x000000, 0);

	// Main group that contains everything
	groups.main = new THREE.Group();
	groups.main.name = 'Main';

	// Group that contains lines for each country
	groups.lines = new THREE.Group();
	groups.lines.name = 'Lines';
	groups.main.add(groups.lines);

	// Group that contains dynamically created dots
	groups.lineDots = new THREE.Group();
	groups.lineDots.name = 'Dots';
	groups.main.add(groups.lineDots);

	// Add the main group to the scene
	scene.add(groups.main);

	// Render camera and add orbital controls
	addCamera();
	addControls();

	// Render objects
	addGlobe();

	if (Object.keys(data.countries).length > 0) {
		addLines();
		createListElements();
	}

	// Start the requestAnimationFrame loop
	render();
	animate();

	var canvasResizeBehaviour = function() {

		container.width = window.innerWidth;
		container.height = window.innerHeight;
		container.style.width = window.innerWidth + 'px';
		container.style.height = window.innerHeight + 'px';

		camera.object.aspect = container.offsetWidth / container.offsetHeight;
		camera.object.updateProjectionMatrix();
		renderer.setSize(container.offsetWidth, container.offsetHeight);

	};

	window.addEventListener('resize', canvasResizeBehaviour);
	window.addEventListener('orientationchange', function() {
		setTimeout(canvasResizeBehaviour, 0);
	});
	canvasResizeBehaviour();

}



/* CAMERA AND CONTROLS */

function addCamera() {

	camera.object = new THREE.PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, 1, 10000);
	camera.object.position.z = props.globeRadius * 2.2;

}

function addControls() {

	camera.controls = new OrbitControls(camera.object, canvas);
	camera.controls.enableKeys = false;
	camera.controls.enablePan = false;
	camera.controls.enableZoom = false;
	camera.controls.enableDamping = false;
	camera.controls.enableRotate = false;

	// Set the initial camera angles to something crazy for the introduction animation
	camera.angles.current.azimuthal = -Math.PI;
	camera.angles.current.polar = 0;

}



/* RENDERING */

function render() {
	renderer.render(scene, camera.object);
}

if ('hidden' in document) {
	document.addEventListener('visibilitychange', onFocusChange);
}
else if ('mozHidden' in document) {
	document.addEventListener('mozvisibilitychange', onFocusChange);
}
else if ('webkitHidden' in document) {
	document.addEventListener('webkitvisibilitychange', onFocusChange);
}
else if ('msHidden' in document) {
	document.addEventListener('msvisibilitychange', onFocusChange);
}
else if ('onfocusin' in document) {
	document.onfocusin = document.onfocusout = onFocusChange;
}
else {
	window.onpageshow = window.onpagehide = window.onfocus = window.onblur = onFocusChange;
}

function onFocusChange(event) {

	var visible = 'visible';
	var hidden = 'hidden';
	var eventMap = {
		focus: visible,
		focusin: visible,
		pageshow: visible,
		blur: hidden,
		focusout: hidden,
		pagehide: hidden
	};

	event = event || window.event;

	if (event.type in eventMap) {
		isHidden = true;
	}
	else {
		isHidden = false;
	}

}

function animate() {

	if (isHidden === false) {
		requestAnimationFrame(animate);
	}

	if (groups.globeDots) {
		introAnimate();
	}

	if (animations.finishedIntro === true) {
		animateDots();
	}

	if (animations.countries.animating === true) {
		animateCountryCycle();
	}

	positionElements();

	camera.controls.update();

	render();

}



/* GLOBE */

function addGlobe() {

	var textureLoader = new THREE.TextureLoader();
	textureLoader.setCrossOrigin(true);

	var radius = props.globeRadius - (props.globeRadius * 0.02);
	var segments = 64;
	var rings = 64;

	// Make gradient
	var canvasSize = 128;
	var textureCanvas = document.createElement('canvas');
	textureCanvas.width = canvasSize;
	textureCanvas.height = canvasSize;
	var canvasContext = textureCanvas.getContext('2d');
	canvasContext.rect(0, 0, canvasSize, canvasSize);
	var canvasGradient = canvasContext.createLinearGradient(0, 0, 0, canvasSize);
	canvasGradient.addColorStop(0, '#5B0BA0');
	canvasGradient.addColorStop(0.5, '#260F76');
	canvasGradient.addColorStop(1, '#130D56');
	canvasContext.fillStyle = canvasGradient;
	canvasContext.fill();

	// Make texture
	var texture = new THREE.Texture(textureCanvas);
	texture.needsUpdate = true;

	var geometry = new THREE.SphereGeometry(radius, segments, rings);
	var material = new THREE.MeshBasicMaterial({
		map: texture,
		transparent: true,
		opacity: 0
	});
	globe = new THREE.Mesh(geometry, material);

	groups.globe = new THREE.Group();
	groups.globe.name = 'Globe';

	groups.globe.add(globe);
	groups.main.add(groups.globe);

	addGlobeDots();

}

function addGlobeDots() {

	var geometry = new THREE.Geometry();

	// Make circle
	var canvasSize = 16;
	var halfSize = canvasSize / 2;
	var textureCanvas = document.createElement('canvas');
	textureCanvas.width = canvasSize;
	textureCanvas.height = canvasSize;
	var canvasContext = textureCanvas.getContext('2d');
	canvasContext.beginPath();
	canvasContext.arc(halfSize, halfSize, halfSize, 0, 2 * Math.PI);
	canvasContext.fillStyle = props.colours.globeDots;
	canvasContext.fill();

	// Make texture
	var texture = new THREE.Texture(textureCanvas);
	texture.needsUpdate = true;

	var material = new THREE.PointsMaterial({
		map: texture,
		size: props.globeRadius / 120
	});

	var addDot = function(targetX, targetY) {

		// Add a point with zero coordinates
		var point = new THREE.Vector3(0, 0, 0);
		geometry.vertices.push(point);

		// Add the coordinates to a new array for the intro animation
		var result = returnSphericalCoordinates(
			targetX,
			targetY
		);
		animations.dots.points.push(new THREE.Vector3(result.x, result.y, result.z));

	};

	for (var i = 0; i < data.points.length; i++) {
		addDot(data.points[i].x, data.points[i].y);
	}

	for (var country in data.countries) {
		addDot(data.countries[country].x, data.countries[country].y);
	}

	// Add the points to the scene
	groups.globeDots = new THREE.Points(geometry, material);
	groups.globe.add(groups.globeDots);

}



/* COUNTRY LINES AND DOTS */

function addLines() {

	// Create the geometry
	var geometry = new THREE.Geometry();

	for (var countryStart in data.countries) {

		var group = new THREE.Group();
		group.name = countryStart;

		for (var countryEnd in data.countries) {

			// Skip if the country is the same
			if (countryStart === countryEnd) {
				continue;
			}

			// Get the spatial coordinates
			var result = returnCurveCoordinates(
				data.countries[countryStart].x,
				data.countries[countryStart].y,
				data.countries[countryEnd].x,
				data.countries[countryEnd].y
			);

			// Calcualte the curve in order to get points from
			var curve = new THREE.QuadraticBezierCurve3(
				new THREE.Vector3(result.start.x, result.start.y, result.start.z),
				new THREE.Vector3(result.mid.x, result.mid.y, result.mid.z),
				new THREE.Vector3(result.end.x, result.end.y, result.end.z)
			);

			// Get verticies from curve
			geometry.vertices = curve.getPoints(200);

			// Create mesh line using plugin and set its geometry
			var line = new MeshLine();
			line.setGeometry(geometry);

			// Create the mesh line material using the plugin
			var material = new MeshLineMaterial({
				color: props.colours.lines,
				transparent: true,
				opacity: props.alphas.lines
			});

			// Create the final object to add to the scene
			var curveObject = new THREE.Mesh(line.geometry, material);
			curveObject._path = geometry.vertices;

			group.add(curveObject);

		}

		group.visible = false;

		groups.lines.add(group);

	}

}

function addLineDots() {

	/*
		This function will create a number of dots (props.dotsAmount) which will then later be
		animated along the lines. The dots are set to not be visible as they are later
		assigned a position after the introduction animation.
	*/

	var radius = props.globeRadius / 120;
	var segments = 32;
	var rings = 32;

	var geometry = new THREE.SphereGeometry(radius, segments, rings);
	var material = new THREE.MeshBasicMaterial({
		color: props.colours.lineDots
	});

	// Returns a sphere geometry positioned at coordinates
	var returnLineDot = function() {
		var sphere = new THREE.Mesh(geometry, material);
		return sphere;
	};

	for (var i = 0; i < props.dotsAmount; i++) {

		// Get the country path geometry vertices and create the dot at the first vertex
		var targetDot = returnLineDot();
		targetDot.visible = false;

		// Add custom variables for custom path coordinates and index
		targetDot._pathIndex = null;
		targetDot._path = null;

		// Add the dot to the dots group
		groups.lineDots.add(targetDot);

	}

}

function assignDotsToRandomLine(target) {

	// Get a random line from the current country
	var randomLine = Math.random() * (animations.countries.selected.children.length - 1);
	randomLine = animations.countries.selected.children[randomLine.toFixed(0)];

	// Assign the random country path to the dot and set the index at 0
	target._path = randomLine._path;

}

function reassignDotsToNewLines() {

	for (var i = 0; i < groups.lineDots.children.length; i++) {

		var target = groups.lineDots.children[i];
		if (target._path !== null && target._pathIndex !== null) {
			assignDotsToRandomLine(target);
		}

	}

}

function animateDots() {

	// Loop through the dots children group
	for (var i = 0; i < groups.lineDots.children.length; i++) {

		var dot = groups.lineDots.children[i];

		if (dot._path === null) {

			// Create a random seed as a pseudo-delay
			var seed = Math.random();

			if (seed > 0.99) {
				assignDotsToRandomLine(dot);
				dot._pathIndex = 0;
			}

		}
		else if (dot._path !== null && dot._pathIndex < dot._path.length - 1) {

			// Show the dot
			if (dot.visible === false) {
				dot.visible = true;
			}

			// Move the dot along the path vertice coordinates
			dot.position.x = dot._path[dot._pathIndex].x;
			dot.position.y = dot._path[dot._pathIndex].y;
			dot.position.z = dot._path[dot._pathIndex].z;

			// Advance the path index by 1
			dot._pathIndex++;

		}
		else {

			// Hide the dot
			dot.visible = false;

			// Remove the path assingment
			dot._path = null;

		}

	}

}



/* ELEMENTS */

var list;

function createListElements() {

 	list = document.getElementsByClassName('js-list')[0];

	var pushObject = function(coordinates, target) {

		// Create the element
		var element = document.createElement('li');

		var innerContent;
		var targetCountry = data.countries[target];

		element.innerHTML = '<span class="text">' + targetCountry.country + '</span>';

		var object = {
			position: coordinates,
			element: element
		};

		// Add the element to the DOM and add the object to the array
		list.appendChild(element);
		elements[target] = object;

	};

	// Loop through each country line
	var i = 0;

	for (var country in data.countries) {

		var group = groups.lines.getObjectByName(country);
		var coordinates = group.children[0]._path[0];
		pushObject(coordinates, country);

		if (country === props.startingCountry) {

			// Set the country cycle index and selected line object for the starting country
			animations.countries.index = i;
			animations.countries.selected = groups.lines.getObjectByName(country);

			// Set the line opacity to 0 so they can be faded-in during the introduction animation
			var lineGroup = animations.countries.selected;
			lineGroup.visible = true;
			for (var ii = 0; ii < lineGroup.children.length; ii++) {
				lineGroup.children[ii].material.uniforms.opacity.value = 0;
			}

			// Set the target camera angles for the starting country for the introduction animation
			var angles = returnCameraAngles(data.countries[country].x, data.countries[country].y);
			camera.angles.target.azimuthal = angles.azimuthal;
			camera.angles.target.polar = angles.polar;

		}
		else {
			i++;
		}

	}

}

function positionElements() {

	var widthHalf = canvas.clientWidth / 2;
	var heightHalf = canvas.clientHeight / 2;

	// Loop through the elements array and reposition the elements
	for (var key in elements) {

		var targetElement = elements[key];

		var position = getProjectedPosition(widthHalf, heightHalf, targetElement.position);

		// Construct the X and Y position strings
		var positionX = position.x + 'px';
		var positionY = position.y + 'px';

		// Construct the 3D translate string
		var elementStyle = targetElement.element.style;
		elementStyle.webkitTransform = 'translate3D(' + positionX + ', ' + positionY + ', 0)';
		elementStyle.WebkitTransform = 'translate3D(' + positionX + ', ' + positionY + ', 0)'; // Just Safari things (capitalised property name prefix)...
		elementStyle.mozTransform = 'translate3D(' + positionX + ', ' + positionY + ', 0)';
		elementStyle.msTransform = 'translate3D(' + positionX + ', ' + positionY + ', 0)';
		elementStyle.oTransform = 'translate3D(' + positionX + ', ' + positionY + ', 0)';
		elementStyle.transform = 'translate3D(' + positionX + ', ' + positionY + ', 0)';

	}

}



/* INTRO ANIMATIONS */

// Easing reference: https://gist.github.com/gre/1650294

var easeInOutCubic = function(t) {
	return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
};

var easeOutCubic = function(t) {
	return (--t) * t * t + 1;
};

var easeInOutQuad = function(t) {
	return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
};

function introAnimate() {

	if (animations.dots.current <= animations.dots.total) {

		var points = groups.globeDots.geometry.vertices;
		var totalLength = points.length;

		for (var i = 0; i < totalLength; i++) {

			// Get ease value
			var dotProgress = easeInOutCubic(animations.dots.current / animations.dots.total);

			// Add delay based on loop iteration
			dotProgress = dotProgress + (dotProgress * (i / totalLength));

			if (dotProgress > 1) {
				dotProgress = 1;
			}

			// Move the point
			points[i].x = animations.dots.points[i].x * dotProgress;
			points[i].y = animations.dots.points[i].y * dotProgress;
			points[i].z = animations.dots.points[i].z * dotProgress;

			// Animate the camera at the same rate as the first dot
			if (i === 0) {

				var azimuthalDifference = (camera.angles.current.azimuthal - camera.angles.target.azimuthal) * dotProgress;
				azimuthalDifference = camera.angles.current.azimuthal - azimuthalDifference;
				camera.controls.setAzimuthalAngle(azimuthalDifference);

				var polarDifference = (camera.angles.current.polar - camera.angles.target.polar) * dotProgress;
				polarDifference = camera.angles.current.polar - polarDifference;
				camera.controls.setPolarAngle(polarDifference);

			}

		}

		animations.dots.current++;

		// Update verticies
		groups.globeDots.geometry.verticesNeedUpdate = true;

	}

	if (animations.dots.current >= (animations.dots.total * 0.65) && animations.globe.current <= animations.globe.total) {

		var globeProgress = easeOutCubic(animations.globe.current / animations.globe.total);
		globe.material.opacity = props.alphas.globe * globeProgress;

		// Fade-in the country lines
		var lines = animations.countries.selected.children;
		for (var ii = 0; ii < lines.length; ii++) {
			lines[ii].material.uniforms.opacity.value = props.alphas.lines * globeProgress;
		}

		animations.globe.current++;

	}

	if (animations.dots.current >= (animations.dots.total * 0.7) && animations.countries.active === false) {

		list.classList.add('active');

		var key = Object.keys(data.countries)[animations.countries.index];
		changeCountry(key, true);

		animations.countries.active = true;

	}

	if (animations.countries.active === true && animations.finishedIntro === false) {

		animations.finishedIntro = true;
		// Start country cycle
		animations.countries.timeout = setTimeout(showNextCountry, animations.countries.initialDuration);
		addLineDots();

	}

}



/* COUNTRY CYCLE */

function changeCountry(key, init) {

	if (animations.countries.selected !== undefined) {
		animations.countries.selected.visible = false;
	}

	for (var name in elements) {
		if (name === key) {
			elements[name].element.classList.add('active');
		}
		else {
			elements[name].element.classList.remove('active');
		}
	}

	// Show the select country lines
	animations.countries.selected = groups.lines.getObjectByName(key);
	animations.countries.selected.visible = true;

	if (init !== true) {

		camera.angles.current.azimuthal = camera.controls.getAzimuthalAngle();
		camera.angles.current.polar = camera.controls.getPolarAngle();

		var targetAngles = returnCameraAngles(data.countries[key].x, data.countries[key].y);
		camera.angles.target.azimuthal = targetAngles.azimuthal;
		camera.angles.target.polar = targetAngles.polar;

		animations.countries.animating = true;
		reassignDotsToNewLines();

	}

}

function animateCountryCycle() {

	if (animations.countries.current <= animations.countries.total) {

		var progress = easeInOutQuad(animations.countries.current / animations.countries.total);

		var azimuthalDifference = (camera.angles.current.azimuthal - camera.angles.target.azimuthal) * progress;
		azimuthalDifference = camera.angles.current.azimuthal - azimuthalDifference;
		camera.controls.setAzimuthalAngle(azimuthalDifference);

		var polarDifference = (camera.angles.current.polar - camera.angles.target.polar) * progress;
		polarDifference = camera.angles.current.polar - polarDifference;
		camera.controls.setPolarAngle(polarDifference);

		animations.countries.current++;

	}
	else {

		animations.countries.animating = false;
		animations.countries.current = 0;

		animations.countries.timeout = setTimeout(showNextCountry, animations.countries.duration);

	}

}

function showNextCountry() {

	animations.countries.index++;

	if (animations.countries.index >= Object.keys(data.countries).length) {
		animations.countries.index = 0;
	}

	var key = Object.keys(data.countries)[animations.countries.index];
	changeCountry(key, false);

}



/* COORDINATE CALCULATIONS */

// Returns an object of 3D spherical coordinates
function returnSphericalCoordinates(latitude, longitude) {

	/*
		This function will take a latitude and longitude and calcualte the
		projected 3D coordiantes using Mercator projection relative to the
		radius of the globe.

		Reference: https://stackoverflow.com/a/12734509
	*/

	// Convert latitude and longitude on the 90/180 degree axis
	latitude = ((latitude - props.mapSize.width) / props.mapSize.width) * -180;
	longitude = ((longitude - props.mapSize.height) / props.mapSize.height) * -90;

	// Calculate the projected starting point
	var radius = Math.cos(longitude / 180 * Math.PI) * props.globeRadius;
	var targetX = Math.cos(latitude / 180 * Math.PI) * radius;
	var targetY = Math.sin(longitude / 180 * Math.PI) * props.globeRadius;
	var targetZ = Math.sin(latitude / 180 * Math.PI) * radius;

	return {
		x: targetX,
		y: targetY,
		z: targetZ
	};

}

// Reference: https://codepen.io/ya7gisa0/pen/pisrm?editors=0010
function returnCurveCoordinates(latitudeA, longitudeA, latitudeB, longitudeB) {

	// Calculate the starting point
	var start = returnSphericalCoordinates(latitudeA, longitudeA);

	// Calculate the end point
	var end = returnSphericalCoordinates(latitudeB, longitudeB);

	// Calculate the mid-point
	var midPointX = (start.x + end.x) / 2;
	var midPointY = (start.y + end.y) / 2;
	var midPointZ = (start.z + end.z) / 2;

	// Calculate the distance between the two coordinates
	var distance = Math.pow(end.x - start.x, 2);
	distance += Math.pow(end.y - start.y, 2);
	distance += Math.pow(end.z - start.z, 2);
	distance = Math.sqrt(distance);

	// Calculate the multiplication value
	var multipleVal = Math.pow(midPointX, 2);
	multipleVal += Math.pow(midPointY, 2);
	multipleVal += Math.pow(midPointZ, 2);
	multipleVal = Math.pow(distance, 2) / multipleVal;
	multipleVal = multipleVal * 0.7;

	// Apply the vector length to get new mid-points
	var midX = midPointX + multipleVal * midPointX;
	var midY = midPointY + multipleVal * midPointY;
	var midZ = midPointZ + multipleVal * midPointZ;

	// Return set of coordinates
	return {
		start: {
			x: start.x,
			y: start.y,
			z: start.z
		},
		mid: {
			x: midX,
			y: midY,
			z: midZ
		},
		end: {
			x: end.x,
			y: end.y,
			z: end.z
		}
	};

}

// Returns an object of 2D coordinates for projected 3D position
function getProjectedPosition(width, height, position) {

	/*
		Using the coordinates of a country in the 3D space, this function will
		return the 2D coordinates using the camera projection method.
	*/

	position = position.clone();
	var projected = position.project(camera.object);

	return {
		x: (projected.x * width) + width,
		y: -(projected.y * height) + height
	};

}


// Returns an object of the azimuthal and polar angles of a given map latitude and longitude
function returnCameraAngles(latitude, longitude) {

	/*
		This function will convert given latitude and longitude coordinates that are
		proportional to the map dimensions into values relative to PI (which the
		camera uses as angles).

		Note that the azimuthal angle ranges from 0 to PI, whereas the polar angle
		ranges from -PI (negative PI) to PI (positive PI).

		A small offset is added to the azimuthal angle as angling the camera directly on top of a point makes the lines appear flat.
	*/

	var targetAzimuthalAngle = ((latitude - props.mapSize.width) / props.mapSize.width) * Math.PI;
	targetAzimuthalAngle = targetAzimuthalAngle + (Math.PI / 2);
	targetAzimuthalAngle = targetAzimuthalAngle + 0.1; // Add a small offset
	
	var targetPolarAngle = (longitude / (props.mapSize.height * 2)) * Math.PI;

	return {
		azimuthal: targetAzimuthalAngle,
		polar: targetPolarAngle
	};

}



/* INITIALISATION */

if (!window.WebGLRenderingContext) {
	showFallback();
}
else {
	getData();
}

            
          
!
999px
Loading ..................

Console