WebGL does not support text. Overlay is a simple class that allows an HTML Canvas rendering 2D to overlay a canvas rendering 3D.
The overlay is able to keep labels synchronized with the 3D view by applying the appropriate view and perspective transformations.
In your initialization code...
const overlay = new Overlay('overlay', camera, 500, 500);
In your animation loop...
overlay.clear();
overlay.text("a", arrowA.h / 2);
overlay.text("b", arrowB.h / 2);
<div id='container'>
<canvas id='canvas3D'></canvas>
<canvas id='canvas2D' width='500' height='500'></canvas>
</div>
#container {
position: relative;
}
#canvas3D {
position: absolute;
left: 0px;
top: 0px;
width: 500px;
height: 500px;
}
#canvas2D {
position: absolute;
left: 0px;
top: 0px;
z-index: 10;
width: 500px;
height: 500px;
/**
* Allow events to go to the other elements
*/
pointer-events: none;
}
export default class Overlay {
public ctx: CanvasRenderingContext2D;
constructor(canvas: string, private camera: EIGHT.PerspectiveCamera) {
const canvasElement = <HTMLCanvasElement>document.getElementById('canvas2D');
this.ctx = canvasElement.getContext('2d');
this.ctx.strokeStyle = "#FFFFFF";
this.ctx.fillStyle = '#ffffff';
this.ctx.font = '24px Helvetica';
}
clear(): void {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
}
fillText(text: string, X: EIGHT.VectorE3, maxWidth?: number) {
const coords = this.canvasCoords(X);
this.ctx.fillText(text, coords.x, coords.y, maxWidth);
}
beginPath() {
this.ctx.beginPath();
}
moveTo(X: EIGHT.VectorE3) {
const coords = this.canvasCoords(X);
this.ctx.moveTo(coords.x, coords.y);
}
lineTo(X: EIGHT.VectorE3) {
const coords = this.canvasCoords(X);
this.ctx.lineTo(coords.x, coords.y);
}
stroke() {
this.ctx.stroke();
}
private canvasCoords(X: EIGHT.VectorE3): EIGHT.VectorE2 {
const camera = this.camera;
const cameraCoords = view(EIGHT.Geometric3.fromVector(X), camera.eye, camera.look, camera.up);
const N = camera.near;
const F = camera.far;
const θ = camera.fov;
const aspect = camera.aspect;
const canonCoords = perspective(cameraCoords, N, F, θ, aspect);
const x = (canonCoords.x + 1) * this.ctx.canvas.width / 2;
const y = (canonCoords.y - 1) * -this.ctx.canvas.height / 2;
return {x, y}
}
}
/**
* View transformation converts world coordinates to camera frame coordinates.
*/
function view(X: EIGHT.Geometric3, eye: EIGHT.Geometric3, look: EIGHT.Geometric3, up: EIGHT.Geometric3): EIGHT.VectorE3 {
const n = (eye - look).normalize();
const u = up.clone().cross(n).normalize();
const v = n.clone().cross(u);
const du = - eye | u;
const dv = - eye | v;
const dn = - eye | n;
const x = (X | u + du).a;
const y = (X | v + dv).a;
const z = (X | n + dn).a;
return {x, y, z};
}
/**
* Perspective transformation projects camera coordinates onto the near plane.
*/
function perspective(X: EIGHT.VectorE3, N: number, F: number, fov: number, aspect: number): EIGHT.VectorE3 {
const t = N * Math.tan(fov / 2);
const b = -t;
const r = aspect * t;
const l = -r;
const x = ((2 * N) * X.x + (r + l) * X.z) / (-X.z * (r-l));
const y = ((2 * N) * X.y + (t + b) * X.z) / (-X.z * (t-b));
const z = (-(F+N) * X.z - 2 * F * N) / (-X.z * (F-N))
return {x, y, z};
}