Visualizing Last.fm data with WebGL, GLGE, jQuery

Posted by Dennis on Jul 23, 2010 in 3D, WebGL3 comments

I’m a jQuery newbie so I thought I’d try it out in combination with WebGL. It makes a great example together with the GLGE library for WebGL.

Mind you, there are better ways to do tunnels in 3D. This demo is meant to demonstrate how specific things work with GLGE.

An explanation of what’s going on can be found below. Click here to see the live demo or watch the YouTube video if you don’t have a WebGL-enabled browser:

Using jQuery to get the data from Last.fm

First there’s a bit of jQuery code that gets the xml and the images from Last.fm. I use a php proxy script to circumvent security restrictions.


var images = [];
var apiKey = 'e351ab3a5309170fa0390aeaebd09304';
/**
 * Last.FM Service Calls
 */
var serviceCalls = [
	'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=napalm%20death&api_key=' + apiKey),
	'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=metallica&api_key=' + apiKey),
	'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=ambulette&api_key=' + apiKey),
	'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=dri&api_key=' + apiKey)
];

/**
 * Start the Last.FM Service Calls
 */
$(document).ready(function()
{
	loadNextService();
});	

/**
 * Get the artists's top albums
 */
function loadNextService()
{
	if(serviceCalls.length == 0)
	{
		start3D();
		return;
	}

	var serviceCall = serviceCalls.shift();

	/**
	 * Traverse the xml file and get the images
	 */
	$.ajax({
		type: "GET",
		url: serviceCall,
		dataType: "xml",
		success: function(xml) {
			$(xml).find('album').each(function(){
				$(this).find('image').each(function(){
					var id = $(this).attr('size');
					if(id == "large")
						images.push($(this).text());
				})
			});
			loadNextService();
		}
	});
}

Creating the tunnel with GLGE

I’m going to skip the basic stuff (setting up the scene, adding textures) that I explained in a previous post (here).
There’s one specific thing regarding the textures though. Last.fm usually returns non power of two textures. These can be used with Minefield, but not with Chromium. This resizing is done server-side with a php script:


var texture = new GLGE.Texture();
texture.setSrc('ResizeImage.php?size=256&url=' + escape(images[i]));

The planes are grouped within GLGE.Group objects. The syntax for this is very simple:


var currentGroup = new GLGE.Group();
group.addObject(myObject);

The rest is self-explanatory:


var images = [];
var apiKey = 'e351ab3a5309170fa0390aeaebd09304';
/**
 * Last.FM Service Calls
 */
var serviceCalls = [
	'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=napalm%20death&api_key=' + apiKey),
	'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=metallica&api_key=' + apiKey),
	'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=ambulette&api_key=' + apiKey),
	'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=dri&api_key=' + apiKey)
];

/**
 * Start the Last.FM Service Calls
 */
$(document).ready(function()
{
	loadNextService();
});	

/**
 * Get the artists's top albums
 */
function loadNextService()
{
	if(serviceCalls.length == 0)
	{
		start3D();
		return;
	}

	var serviceCall = serviceCalls.shift();

	/**
	 * Traverse the xml file and get the images
	 */
	$.ajax({
		type: "GET",
		url: serviceCall,
		dataType: "xml",
		success: function(xml) {
			$(xml).find('album').each(function(){
				$(this).find('image').each(function(){
					var id = $(this).attr('size');
					if(id == "large")
						images.push($(this).text());
				})
			});
			loadNextService();
		}
	});
}		

/**
 * Set up the 3D scene
 */
function start3D() {
	var doc = new GLGE.Document();
	var planeGroups = [];
	var circleRadius = 5;
	var degreeStep = 30;
	var zStep = -11;
	var firstGroup;
	var lastGroup;

	doc.onLoad = function() {
		var renderer = new GLGE.Renderer(document.getElementById("canvas"));
		var scene = doc.getElement("mainscene");
		camera = scene.getCamera();
		renderer.setScene(scene);

		var planesPerRing = Math.ceil(360/degreeStep);
		var numPlanes = images.length - (images.length % planesPerRing);
		var currentGroup = new GLGE.Group();
		firstGroup = currentGroup;
		planeGroups.push(currentGroup);
		scene.addObject(currentGroup);

		for(var i=0;i<=numPlanes;i++){
			var plane = new GLGE.Object();
			plane.setMesh(doc.getElement("Plane"));

			var material = new GLGE.Material();
			var texture = new GLGE.Texture();
			var materialLayer = new GLGE.MaterialLayer();

			texture.setSrc('ResizeImage.php?size=256&url=' + escape(images[i]));

			materialLayer.setMapinput(GLGE.UV1);
			materialLayer.setMapto(GLGE.M_COLOR);
			materialLayer.setTexture(texture);

			material.addTexture(texture);
			material.addMaterialLayer(materialLayer);

			plane.setMaterial(material);

			var degrees = (i * degreeStep) % 360;
			var radians = ( Math.PI / 180 ) * degrees;
			var degreesTan = ( degrees + 90 ) % 360;
			var radiansTan = ( Math.PI / 180 ) * degreesTan;

			plane.setScale(1.3, 5.2, 1.3);
			plane.setRotX(Math.PI * .5);
			plane.setRotY(radiansTan);
			plane.setLocX(Math.cos(radians) * circleRadius);
			plane.setLocY(Math.sin(radians) * circleRadius);

			currentGroup.addObject(plane);

			if(i%planesPerRing==0 && i > 0 && i != numPlanes)
			{
				var newGroup = new GLGE.Group();
				newGroup.setLocZ(planeGroups.length*zStep);
				currentGroup.nextGroup = newGroup;
				currentGroup = newGroup;
				scene.addObject(currentGroup);
				planeGroups.push(currentGroup);
			}
		}

		var locXDispl = 0;
		var rotDispl = 0;

		function render() {
			renderer.render();
			var group = firstGroup;

			do {
				group.setLocZ(group.getLocZ() + 1.5);
				group = group.nextGroup;
			} while(group);

			if(firstGroup.getLocZ() > .5) {
				firstGroup.setLocZ(lastGroup.getLocZ() + zStep);
				lastGroup.nextGroup = firstGroup;
				var nextGroup = firstGroup.nextGroup;
				firstGroup.nextGroup = null;
				lastGroup = firstGroup;
				firstGroup = nextGroup;
			}

			var rotDisplSin = Math.sin(rotDispl);
			camera.setLocX(Math.sin(locXDispl) * 3);
			camera.setRotY(rotDisplSin * .3);
			camera.setRotZ(rotDisplSin * .5);

			locXDispl += .05;
			rotDispl += .02;
		}

		lastGroup = currentGroup;
		setInterval(render, 1000/60);
	}

	doc.load("scene.xml");
}


Tags: , , , , ,


3 comments

» Comments RSS Feed
  1. [...] A bit of a mash-up from Dennis Ippel: visualising Last.fm data with WebGL, GLGE and jQuery. [...]

  2. This looks great from the youtube video, but when I try in Firefox nightly, I get errors (series of blank alert dialogs). A lot of WebGL demos seem to have broken recently, some API changes perhaps?

  3. I retract my comment about it not working in FF4 nightlies; it works in FF4beta4, which is more important :) nice work!

Leave a comment