three.js clock

Following the three.js charts post, here’s the clock using three.js:

class ThreeJsClock
  initCSR: ->
    width = 800
    height = 600
    viewAngle = 45
    aspect = width / height
    near = 1
    far = 1000
    
    @camera = new THREE.PerspectiveCamera viewAngle, aspect, near, far
    @camera.position.z = 500
    
    @scene = new THREE.Scene
    @scene.add @camera

    @renderer = new THREE.WebGLRenderer
      antialias: true
      premultipliedAlpha: false
      alpha: true
    @renderer.setSize width, height
    
    canvas = $(@renderer.domElement)
      .attr('id', 'canvas')
      
    $('<div>')
      .append(canvas)
      .appendTo(document.body)
    
  draw: =>
    @render()
    setTimeout(() =>
        requestAnimationFrame @draw
      , 1000 / @fps)

  setHands: =>
    time = new Date()
    
    @handsSep.second.rotation.z = -time.getSeconds() / 60 * 2 * Math.PI
    @handsSep.minute.rotation.z = -time.getMinutes() / 60 * 2 * Math.PI
    @handsSep.hour.rotation.z = -time.getHours() / 12 * 2 * Math.PI
    
  render: =>
    if Math.abs(@group.rotation.y) > Math.PI / 2
      @rot *= -1
    @group.rotation.y += @rot
    
    @setHands()
    @renderer.render @scene, @camera
    
  makeScene: ->
    @group = new THREE.Object3D
    @scene.add @group
        
    @clock = new THREE.Object3D
    @group.add @clock
    
    backGeom = new THREE.CylinderGeometry(
      radiusTop = 200, 
      radiusBottom = 200, 
      height = 10, 
      radiusSegments = 70,
      heightSegments = 70, 
      openEnded = false,
      )
    
    backMat = new THREE.MeshLambertMaterial
      color: 0x80b2ff
      
    backMesh = new THREE.Mesh backGeom, backMat
    backMesh.rotation.x = Math.PI / 2
    @clock.add backMesh
    
    @ticks = new THREE.Object3D
    @clock.add @ticks
    
    tickGeom = new THREE.SphereGeometry(
      radius = 10, 
      widthSegments = 70, 
      heightSegments = 70)
    
    tickMat = new THREE.MeshLambertMaterial
      color: 0xcfcf53
      
    for x in [0..12]
      tickMesh = new THREE.Mesh tickGeom, tickMat
      tickMesh.position.y = 180
      
      dummy = new THREE.Object3D
      dummy.add tickMesh
      dummy.rotation.z = x * Math.PI / 6
      
      @ticks.add dummy

    @hands = new THREE.Object3D
    @clock.add @hands

    handCentGeom = new THREE.SphereGeometry(
      radius = 25, 
      widthSegments = 70, 
      heightSegments = 70)
    
    handCentMat = new THREE.MeshLambertMaterial
      color: 0xffc0c0
    
    handCentMesh = new THREE.Mesh handCentGeom, handCentMat
    @hands.add handCentMesh
      
    makeHand = (hands, len, rad, z) ->
      handGeom = new THREE.CylinderGeometry(
        radiusTop = 1, 
        radiusBottom = rad, 
        height = len, 
        radiusSegments = 70,
        heightSegments = 70, 
        openEnded = false,
        )
      handMat = new THREE.MeshLambertMaterial
        color: 0xffc0c0
        
      handMesh = new THREE.Mesh handGeom, handMat
      handMesh.position.y = len / 2
      handMesh.position.z = z

      dummy = new THREE.Object3D
      dummy.add handMesh
      
      hands.add dummy
      dummy
      
    @handsSep = 
      second: makeHand @hands, 170, 3, 20
      minute: makeHand @hands, 150, 4, 15
      hour: makeHand @hands, 130, 5, 10
            
    @rot = 0.01
    
    directionalLight = new THREE.SpotLight 0xffffff
    directionalLight.position.set 0, 0, 3000
    @scene.add directionalLight
    
  run: ->
    @initCSR()
    @makeScene()
    
    @fps = 15
    @draw()
    
module.exports = ThreeJsClock

Here’s a screenshot:

screenshot


WebGL charts

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:

screenshot


Brunch / Marionette sample application

The other day I ran into Brunch. You can visit the site to get more information, but in a nutshell it’s a build tool. I decided to play with it a bit.

Installation

Installation is simple:

$ npm install -g brunch

Creating a sample project

In order to create a new project, a skeleton is used. At the moment, there are a few dozen skeletons listed here. I am going to use this, which is “Brunch with Coffee Script, Stylus, Backbone, Marrionette, and jQuery.”.

To create the project, this needs to be done:

$ brunch new https://github.com/monokrome/brunch-with-grits

After the skeleton is cloned and the libraries downloaded, you will have something like this:

app
bower_components
bower.json
config.coffee
node_modules
package.json
README.md
vendor

Here:

  • config.coffee is a brunch configuration
  • bower.json is bower configuration
  • vendor is where brunch will put the bower-fetched dependencies (which are managed by bower under bower_components)
  • app is where files related to our application should go

In order to build it for the first time, do this:

$ npm install
$ brunch build

After this, another folder appears – public. This one contains compiled files:

index.html
javascripts
stylesheets

Watching

When developing, all the compilation that needs to be done can be done automatically. This is the watching functionality of brunch. Just invoke this:

$ brunch watch --server

The --server parameter will start a development Web server (defaults to localhost:3333).

Pieces

The above skeleton already contains application.coffee file. This one initializes Backbone.Marionette.Application and does nothing else. In order to display something, Marionette has a few concepts. This is I think best explained on the main page here, but briefly:

  • Template translates into a HTML that will be shown
  • View renders a template and handles events
  • Region defines a jQuery selector that will hold a View instance

View also depends on Backbone.Model or Backbone.Collection to hold the data that is used by a Template instance when rendering.

Basic app

Let’s do a small app – a bounded (to [-5, 5] integer range) counter. In brief:

  • Our model is a number
  • Our view is a text box and a button
  • Button click will make the model’s number to be increased by one

I’m also going to use Handlebars templates, so will first add this:

    "handlebars-brunch": ">= 1.0 < 1.8"

to the dependencies secdion of package.json, followed by:

$ npm install

Let's do this step by step.

Modules

I'm going to use the regular CommonJS modules here. Thus, when I say "this goes to file abc", it needs to be put in that file for the require logic to work. If you change the file names or the folders where they belong, please update the require lines appropriately. Just to make it simple, all files here will go to the root of app folder.

Main HTML

The main HTML in the skeleton is not surprisingly index.html. We just need to add one line in the body:

    <div id="main"></div>

As to why, it will be clear when we discuss the controller a bit later. This edit goes to assets/index.html.

Model

Our model is simple:

module.exports = class Number extends Backbone.Model
  defaults:
    number: 1

  inc: ->
    @set 'number', @get('number') + 1

This goes to number-model.coffee. So we have:

  • Number class that extends Backbone.Model class
  • Default number is 1
  • Inc method that increases the number property
  • Emphasis on property here - calling set on number will send a change:number event, which we will use later

Template

Template is simple as well:

<input id="number" type="text" value="{{number}}" readonly="false" />
<button id="inc">Inc</button>

This goes to number-template.hbs. We have this:

  • This is a Handlebars template
  • It contains two HTML elements - an input box to display our number
  • Both have the IDs that we'll reference later (#number and #inc)
  • Input has it's value set to {{number}} which Handlebars will render appropriately
  • It also has readonly set to false, just so user cannot change the value, as we don't have the binding for this update

View

View is simple, though a bit more complex than model:

module.exports = class NumberView extends Backbone.Marionette.ItemView
  template: require('number-template')
  events:
    'click #inc': 'inc'
  ui:
    incInp: '#number'

  initialize: ->
    @listenTo @model, "change:number", @numberChanged

  numberChanged: ->
    @ui.incInp.attr 'value', @model.get 'number'

  inc: (evt) ->
    @trigger 'number:inc'

This goes to number-view.coffee. Here we have:

  • NumberView extends Backbone.Marionette.ItemView. This class is responsible for showing a single item, hence the name
  • We specify the template as number-template
  • The events says that when our button (HTML ID is #inc, our Inc button) is clicked, we invoke inc method on the view
  • ui property is just an alias map. It allows us to say @ui.incInp and reference the HTML element specified, in this case #number, which is our input element
  • initialize method binds a listener to our model. It says: whenever the model triggers change:number event, call our numberChanged function on this view
  • numberChanged is just updating the input with the number whenever it changes
  • inc method is the event listener for button click - it triggers number:inc event, which our controller is going to catch and then act appropriately

Controller

The controller looks like this:

module.exports = class NumberController extends Marionette.Controller
  initialize: (options) ->
    @model = options.model
    @view = options.view
    @listenTo @view, 'number:inc', @numberInc

  numberInc: ->
    newNumber = @model.get 'number'
    newNumber++
    newNumber = -5 if newNumber > 5
    @model.set 'number', newNumber

Note that this (as per this doc paragraph) is not equivalent to the C in MVC. I'm however using it for this purpose only.

This goes to number-controller.coffee. It has two things:

  • initialize method remembers the provided model and view instances and subscribes to number:inc event. This event is triggered from view when button is clicked, see above
  • numberInc is the method that encapsulates the business logic of our bounded counter. It will increase the number and make sure it stays in [-5, 5] range
  • Just a reminder here: doing @model.set will trigger a change:number event, which will complete the cycle by going to our view and rendering it via numberChanged method, see above

Application initialization - wiring all this together

We need to update application.coffee to wire this together. We can also do this in initialize.coffee, depending on how we want to do the separation of concerns. Here's how it looks:

NumberModel = require('number-model')
NumberView = require('number-view')
NumberController = require('number-controller')

class Application extends Backbone.Marionette.Application
  initialize: =>
    @on 'initialize:after', @startHistory

    model = new NumberModel
    view = new NumberView model: model
    new NumberController model: model, view: view

    @addRegions mainRegion: '#main'
    @mainRegion.show view

    @start()

  startHistory: (options) => Backbone.history.start()

module.exports = Application

Here we do the following:

  • Import the necessary classes, NumberModel, NumberView and NumberController
  • In initialize, instantiate the model, view and controller and wire them up as needed
  • Add a region - this references the #main div that we added to index.html
  • Finally, show the view into this region and start the app

Conclusion

I can say this about the simple app:

  • Brunch makes it very easy to start the project - installation and getting up and running from the appropriate skeleton is just a few commands away
  • It's watch command is a one-step way to a very good development experience - it compiles templates, CoffeeScript files, bundles everything and a refresh of the dev Web server just brings the latest - like it a lot
  • Bower allows for a very nice dependency management
  • Marionette provides a very good and simple framework for decoupling things into sensible, well-contained units (models, templates, views, controllers, application - and there are also layouts and under-the-cover event aggregator that we actually used above)

All in all, I think this is a very good model - I hope to use it going forward.