I wanted to see how hard it would be to make a WebGL chart. Here’s the code with comments. The code is written in CoffeeScript.
class ThreeJsPrb initCSR: -> width = 800 height = 600 viewAngle = 45 aspect = width / height near = 0.1 far = 20000 @camera = new THREE.PerspectiveCamera viewAngle, aspect, near, far @camera.position.z = 15000 @scene = new THREE.Scene @scene.add @camera @renderer = new THREE.WebGLRenderer antialias: true premultipliedAlpha: false @renderer.setSize width, height $('<div>') .append(@renderer.domElement) .appendTo(document.body)
We are making a class called ThreeJsPrb. The first method is called initCSR, short for init camera, scene and renderer. WebGL needs these three:
- Camera to define where we are looking from
- Scene that defines what the world contains
- Rendered to render all this
In the above:
- We define a few variables defining dimensions of the renderer, view angle aspect and near / far z-index
- Setup a camera and it’s position
- Setup a scene and add a camera to it
- Setup a renderer, with some options (antialias and premultipliedAlpha) and set it’s weight
- Finally, add the renderer to dom using jQuery
draw: => @render() setTimeout(() => requestAnimationFrame @draw , 1000 / @fps)
This method is the main “loop” of this app. It will render the current scene, then use a combination of setTimeout and requestAnimationFrame to repeat this in the future. The combination in this case is necessary to be able to set a relatively constant fps that doesn’t max out the fps all the time.
render: => if Math.abs(@group.rotation.y) > Math.PI / 8 @rot *= -1 @group.rotation.y += @rot @renderer.render @scene, @camera
The render method itself does two things:
- Applies a see-saw-behaving rotation (i.e. rotate right up to Math.PI / 8 radians, then reverse the direction)
- Renders the scene using the given camera
# http://en.wikipedia.org/wiki/World_population#Population_growth_by_region worldPopulation: { 1: 200 1000: 310 1750: 791 1800: 978 1850: 1262 1900: 1650 1950: 2519 1955: 2756 1960: 2982 1965: 3335 1970: 3692 1975: 4068 1980: 4435 1985: 4831 1990: 5263 1995: 5674 2000: 6070 2005: 6454 2010: 6972 }
This is just a sample chart data, taken from Wikipedia.
makeScene: -> material = new THREE.MeshLambertMaterial color: 0x80b2ff @group = new THREE.Object3D @scene.add @group @graph = new THREE.Object3D @graph.position.y -= 3000 @group.add @graph xpos = 0 radius = 200 for k, v of @worldPopulation radiusTop = radius radiusBottom = radius height = v radiusSegments = 50 heightSegments = 50 openEnded = false cylinderGeometry = new THREE.CylinderGeometry radiusTop, radiusBottom, height, radiusSegments, heightSegments, openEnded cylinder = new THREE.Mesh cylinderGeometry, material cylinder.position.x = xpos cylinder.position.y = v / 2 xpos += radius * 3 @graph.add cylinder @graph.position.x = -xpos / 2 @rot = 0.01 @group.rotation.x = Math.PI / 8 directionalLight = new THREE.SpotLight 0xffffff directionalLight.position.set -xpos, 1000, 3000 directionalLight.angle = Math.PI / 2 directionalLight.target.position.set xpos / 3, 0, 0 @scene.add directionalLight
This is the meat of the app. The above code is responsible for setting up the scene. As our scene doesn’t change except for rotation, this is done only once. Here’s what’s going on:
- We define a lambert material of a given bluish color
- We define a Object3D called group. This will be used for easier around-the-center rotation later
- We create another Object3D called graph and add it to the group. We also move it “down” by setting the y-axis position
- For each of the keys in worldPopulation map, we create a cylinder geometry and a mesh out of this and the material we made above. We add that to the graph
- We move the graph within it’s parent element (group) to be split in the middle along x-axis. Again, this is to achieve around-the-center rotation later
- We setup the rotation step
- We add a direction light
run: -> console.log '> ThreeJsPrb.run' @initCSR() @makeScene() @fps = 15 @draw() console.log '< ThreeJsPrb.run' module.exports = ThreeJsPrb
Finally, the initialization code and module export.
Here’s a screenshot: