Tuesday, February 26, 2013

Movable, Centered Labels for Three.js Objects

‹prev | My Chain | next›

Up today, I hope to build on the Three.js label maker library from yesterday. In other words, I hope to make it into a library.

So far, I can label a single object on the scene:



I need to be able to easily label multiple objects and the location of the label should be above the object, not to the side.

Multiple object labels means that I need to take yesterday's procedural code and turn it into a class. I start with a constructor that requires the object being labeled, the camera, and the text content:
  function Label(object, camera, content) {
    this.object = object;
    this.camera = camera;
    this.content = content;

    this.el = this.buildElement();
    this.track();
  }
The buildElement() method does the usual DOM building:
  Label.prototype.buildElement = function() {
    var el = document.createElement('div');
    el.textContent = this.content;
    el.style.backgroundColor = 'white';
    el.style.position = 'absolute';
    el.style.padding = '1px 4px';
    el.style.borderRadius = '2px';
    el.style.maxWidth = (window.innerWidth * 0.25) + 'px';
    el.style.maxHeight = (window.innerHeight * 0.25) + 'px';
    el.style.overflowY = 'auto';
    document.body.appendChild(el);
    return el;
  };
The track() method is where the projection from 3D space into web page 2D takes place:
  Label.prototype.track = function() {
    var p3d = this.object.position.clone();

    var projector = new THREE.Projector(),
        pos = projector.projectVector(p3d, camera),
        width = window.innerWidth,
        height = window.innerHeight,
    this.el.style.top = '' + (height/2 - height/2 * pos.y) + 'px';
    this.el.style.left = '' + (width/2 * pos.x + width/2) + 'px';
    var that = this;
    setTimeout(function(){that.track();}, 1000/60);
  };
All of that is from yesterday, with the exception of that last two lines. I update the label tracking 60 times a second. I can likely get away with a slower update rate, but that does not tax the CPU and makes for a smooth animation around the screen.

It is in this track() method that I can improve on the positioning of the label. Firstly, I can move the 3D point up a bit. Since I want the label above the object, I increase the 3D position by the Three.js boundRadius. While I am at it, I offset the web page positioning by the DOM element's width and height:
  Label.prototype.track = function() {
    var p3d = this.object.position.clone();
    p3d.y = p3d.y + this.object.boundRadius;    

    var projector = new THREE.Projector(),
        pos = projector.projectVector(p3d, camera),
        width = window.innerWidth,
        height = window.innerHeight,
        w = this.el.offsetWidth,
        h = this.el.offsetHeight;
    this.el.style.top = '' + (height/2 - height/2 * pos.y - 1.5*h) + 'px';
    this.el.style.left = '' + (width/2 * pos.x + width/2 - w/2) + 'px';

    var that = this;
    setTimeout(function(){that.track();}, 1000/60);
  };
That should do it. I can then create multiple labels:
  new Label(earth, camera, "Earth");
  new Label(mars, camera, "Mars");
With that, I have multiple labels that are centered above the corresponding Three.js object:



The labels move with the planets as they orbit the Sun. I am not quite sure that this always works with all camera orientations. The API is a bit unwieldy as well as it requires the camera as the third wheel constructor argument. There is no easy way to avoid it, however, since the camera is necessary in order to make the projection in to 2D space.

I will sleep on the current approach. Perhaps tomorrow I will pick back up by applying this library as speech bubbles in the game player simulation.


Day #674

No comments:

Post a Comment