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.