// src/shader.js
class Shader {
constructor(app) {
this.gl = app.gl;
this.app = app;
this.program = this.gl.createProgram();
}
attach(type, source) {
console.log("attaching shader", { type, source });
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
throw new Error("An error occurred compiling the shaders: " + this.gl.getShaderInfoLog(shader));
}
this.gl.attachShader(this.program, shader);
return this;
}
link() {
this.gl.linkProgram(this.program);
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
throw new Error("Unable to initialize the shader program: " + this.gl.getProgramInfoLog(this.program));
}
console.log("shader linked");
return this;
}
location(name) {
if (name[0] === "a") {
return this.gl.getAttribLocation(this.program, name);
} else if (name[0] === "u") {
return this.gl.getUniformLocation(this.program, name);
}
}
updateTime() {
const time = this.app.now();
const sinTime = Math.sin(time);
const cosTime = Math.cos(time);
this.gl.uniform1f(this.location("uTime"), time);
this.gl.uniform1f(this.location("uSinTime"), sinTime);
this.gl.uniform1f(this.location("uCosTime"), cosTime);
}
activate(projectionMatrix, modelViewMatrix) {
this.gl.useProgram(this.program);
this.gl.uniformMatrix4fv(this.location("uProjectionMatrix"), false, projectionMatrix);
this.gl.uniformMatrix4fv(this.location("uModelViewMatrix"), false, modelViewMatrix);
this.updateTime();
}
}
// src/object.js
class Object {
constructor(app) {
this.gl = app.gl;
this.app = app;
this.vertexPositions = new Float32Array([]);
this.positionBuffer = null;
this.textureBuffer = null;
}
initBuffer(data, draw = this.gl.STATIC_DRAW) {
const buffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(data), draw);
return buffer;
}
attachShader(shader) {
this.shader = shader;
this.vertexPosition = shader.location("aVertexPosition");
this.textureCoord = shader.location("aTextureCoord");
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
this.gl.vertexAttribPointer(this.vertexPosition, 2, this.gl.FLOAT, false, 0, 0);
this.gl.enableVertexAttribArray(this.vertexPosition);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.textureBuffer);
this.gl.vertexAttribPointer(this.textureCoord, 2, this.gl.FLOAT, false, 0, 0);
this.gl.enableVertexAttribArray(this.textureCoord);
return this;
}
draw2D() {
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
this.shader.activate(this.app.projectionMatrix, this.app.modelViewMatrix);
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, this.vertexPositions.length / 2);
}
draw3D() {
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
this.shader.activate(this.app.projectionMatrix, this.app.modelViewMatrix);
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, this.vertexPositions.length / 3);
}
}
// src/basic-plane.js
class BasicPlane extends Object {
constructor(app) {
super(app);
this.vertexPositions = new Float32Array([
-1,
-1,
1,
-1,
-1,
1,
1,
1
]);
this.positionBuffer = this.initBuffer(this.vertexPositions);
this.textureBuffer = this.initBuffer(new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]));
this.app.onUpdate(() => this.draw2D());
}
}
// src/telemetry.js
class Telemetry {
constructor(app, selector = "#telemetry") {
this.app = app;
this.el = document.querySelector(selector);
if (this.el && location.search.includes("telemetry")) {
this.el.style.display = "block";
this.app.onAfterUpdate(() => this.onAfterUpdate());
}
this.frameTimes = [];
this.maxFrameTimes = 100;
this.lastFrameTime = 0;
}
insertTime(time) {
this.frameTimes.push(time);
if (this.frameTimes.length > this.maxFrameTimes) {
this.frameTimes.shift();
}
}
onAfterUpdate() {
const frameTime = this.app.now() - this.lastFrameTime;
this.insertTime(frameTime);
const averageFrameTime = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;
const framesPerSecond = 1000 / averageFrameTime;
this.el.innerHTML = `
${framesPerSecond.toFixed(1)} FPS (${averageFrameTime.toFixed(3)} ms)
bU: ${this.app.registry.onBeforeUpdate.length} | U: ${this.app.registry.onUpdate.length} | aU: ${this.app.registry.onAfterUpdate.length}
`;
this.lastFrameTime = this.app.now();
}
}
// src/app.js
class App {
constructor(config = {
fov: 45
}) {
this._now = 0;
this.registry = {
onStart: [],
onUpdate: [],
onBeforeUpdate: [],
onAfterUpdate: []
};
this.config = config;
this.canvas = document.querySelector("canvas");
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.gl = this.canvas.getContext("webgl2");
if (this.gl === null) {
document.querySelector("main").innerHTML = `