Text Overlay for ThreeJS with Angular and TypeScript

This is not my first foray into WebGL. The last time I was working on a 3D charting API using the YUI framework, which could do things like this:

Personally, I can’t do any debugging at 30fps without having a live list of debugging text that I can watch. So almost immediately after the ‘hello world’ spinning cube, I set that up. And now I’m in the middle of moving my framework over to Angular and TypeScript. For the most part, I like how things are working out, but when it comes to lining up a transparent text plane over a threeJS element, YUI gives a lot more support than Angular. The following is so brute-force that I feel like I must be doing it wrong (And there may be a jquery-lite pattern, but after trying a few StackOverflow suggestions that didn’t work), I went with the following.

First, this all happens in the directive. I try to keep that pretty clean:

// The webGL directive. Instantiates a webGlBase-derived class for each scope
export class ngWebgl {
   private myDirective:ng.IDirective;

   constructor() {
      this.myDirective = null;
   }

   private linkFn = (scope:any, element:any, attrs:any) => {
      //var rb:WebGLBaseClasses.RootBase = new WebGLBaseClasses.RootBase(scope, element, attrs);
      var rb:WebGlRoot = new WebGlRoot(scope, element, attrs);
      scope.webGlBase = rb;
      var initObj:any = {
         showStage: true
      };
      rb.initializer(initObj);
      rb.animate();
   };

   public ctor = ():ng.IDirective => {
      if (!this.myDirective) {
         this.myDirective = {
            restrict: 'AE',
            scope: {
               'width': '=',
               'height': '=',
            },
            link: this.linkFn
         }
      }
      return this.myDirective;
   }
}

The interface with all the webGL code happens in the linkFn() method. Note that the WebGLRoot class gets assigned to the scope. This allows for multiple canvases.

WebGLRoot is a class that inherits from WebGLBaseClasses.CanvasBase, which is one of the two big classes I’m currently working on. It’s mostly there to make sure that everything inherits correctly and I don’t break that without noticing:-)

Within WebGLBaseClasses.CanvasBase is the initializer() method. That in turn calls the methods that set up the WebGL and the ‘stage’ that I want to interact with. The part we’re interested for our overlay plane is the overlay canvas’ context. You’ll needthat  to draw into later:

overlayContext:CanvasRenderingContext2D;

This is set up along with the renderer. Interesting bits are in bold:

this.renderer = new THREE.WebGLRenderer({antialias: true});
this.renderer.setClearColor(this.blackColor, 1);
this.renderer.setSize(this.contW, this.contH);

// element is provided by the angular directive
this.renderer.domElement.setAttribute("class", "glContainer");
this.myElements[0].appendChild(this.renderer.domElement);

var overlayElement:HTMLCanvasElement = document.createElement("canvas");
overlayElement.setAttribute("class", "overlayContainer");
this.myElements[0].appendChild(overlayElement);
this.overlayContext = this.overlayElement.getContext("2d");

The first thing to notice is that I have to add CSS classes to the elements. These are pretty simple, just setting absolute and Z-index:

.glContainer {
    position: absolute;
    z-index: 0;
}

.overlayContainer {
    position: absolute;
    z-index: 1;
}

That forces everything to have the same upper left corner. And once that problem was solved, drawing is pretty straightforward. The way I have things set up is with an animate method that uses requestAnimationFrame() wich then calls the render() method. That draws the 3D, and then hands the 2D context off to the draw2D() method:

draw2D = (ctx:CanvasRenderingContext2D):void =>{
   var canvas:HTMLCanvasElement = ctx.canvas;
   canvas.width = this.contW;
   canvas.height = this.contH;
   ctx.clearRect(0, 0, canvas.width, canvas.height);
   ctx.font = '12px "Times New Roman"';
   ctx.fillStyle = 'rgba( 255, 255, 255, 1)'; // Set the letter color
   ctx.fillText("Hello, framecount: "+this.frameCount, 10, 20);
};

render = ():void => {
   // do the 3D rendering
   this.camera.lookAt(this.scene.position);
   this.renderer.render(this.scene, this.camera);
   this.frameCount++;

   this.draw2D(this.overlayContext);
};

I’m supplying links to to the running code and directives, but please bear in mind that this is in-process development and not an minimal application for clarity.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s