<wc-geo-gl></wc-geo-gl>
function getProjectionMatrix(screenHeight, screenWidth, fieldOfView, zNear, zFar){
	const aspectRatio = screenHeight / screenWidth;
	const fieldOfViewRadians = fieldOfView * (Math.PI / 180);
	const fovRatio = 1 / Math.tan(fieldOfViewRadians / 2);

	return [
		aspectRatio * fovRatio, 0       , 0				  			 , 0,
		0					   , fovRatio, 0				  			 , 0,
		0					   , 0		 , zFar/(zFar - zNear)			 , 1,
		0					   , 0		 , (-zFar * zNear)/(zFar - zNear), 0
	];
}

function compileShader(context, text, type){
	const shader = context.createShader(type);
	context.shaderSource(shader, text);
	context.compileShader(shader);

	if (!context.getShaderParameter(shader, context.COMPILE_STATUS)) {
		throw new Error(`Failed to compile shader: ${context.getShaderInfoLog(shader)}`);
	}
	return shader;
}

function compileProgram(context, vertexShader, fragmentShader){
	const program = context.createProgram();
	context.attachShader(program, vertexShader);
	context.attachShader(program, fragmentShader);
	context.linkProgram(program);

	if (!context.getProgramParameter(program, context.LINK_STATUS)) {
		throw new Error(`Failed to compile WebGL program: ${context.getProgramInfoLog(program)}`);
	}

	return program;
}

class WcGeoGl extends HTMLElement {
	static observedAttributes = ["image", "height", "width"];
	#height = 720;
	#width = 1280;
	#image;
	constructor() {
		super();
		this.bind(this);
	}
	bind(element) {
		element.attachEvents = element.attachEvents.bind(element);
		element.cacheDom = element.cacheDom.bind(element);
		element.createShadowDom = element.createShadowDom.bind(element);
		element.bootGpu = element.bootGpu.bind(element);
		element.render = element.render.bind(element);
	}
	async connectedCallback() {
		this.createShadowDom();
		this.cacheDom();
		this.attachEvents();
		await this.bootGpu();
		this.render();
	}
	createShadowDom() {
		this.shadow = this.attachShadow({ mode: "open" });
		this.shadow.innerHTML = `
				<style>
					:host { display: block; }
				</style>
				<canvas width="${this.#width}px" height="${this.#height}px"></canvas>
			`;
	}
	cacheDom() {
		this.dom = {};
		this.dom.canvas = this.shadow.querySelector("canvas");
	}
	attachEvents() {

	}
	async bootGpu() {
		this.context = this.dom.canvas.getContext("webgl");
		this.program = this.context.createProgram();
	
		const vertexShader = compileShader(this.context, `
				uniform mat4 uProjectionMatrix;
				attribute vec3 aVertexPosition;
				attribute vec3 aVertexColor;

				float angle = -3.1415962 / 4.0;

				mat4 rotationY = mat4(
					cos(angle), 0, sin(angle), 0,
					0, 1, 0, 0,
					-sin(angle), 0, cos(angle), 0,
					0, 0, 0, 1
				);

				vec4 translateZ = vec4(0.0, 0.0, 2.0, 0.0);

				varying mediump vec4 vColor;

				void main(){
					gl_Position = uProjectionMatrix * (translateZ + rotationY * vec4(aVertexPosition, 1.0));
					vColor = vec4(aVertexColor, 1.0);
				}
			`, this.context.VERTEX_SHADER);

		const fragmentShader = compileShader(this.context, `
		    varying lowp vec4 vColor;
			void main() {
				gl_FragColor = vColor;
			}
		`, this.context.FRAGMENT_SHADER);

		this.program = compileProgram(this.context, vertexShader, fragmentShader)

		this.context.useProgram(this.program);
		this.createPositions();
		this.createColors();
		//this.createUvs();
		this.createIndices();
		this.setupUniforms();

		//this.createTexture(await loadImage(this.getAttribute("image")));

		this.context.enable(this.context.CULL_FACE);
		this.context.cullFace(this.context.BACK);
	}
	createPositions() {
		const positionBuffer = this.context.createBuffer();
		this.context.bindBuffer(this.context.ARRAY_BUFFER, positionBuffer);

		const positions = new Float32Array([
			//Front
			-0.5, -0.5, -0.5,
			 0.5, -0.5, -0.5,
			 0.5,  0.5, -0.5,
			-0.5,  0.5, -0.5,
			//Back
			 0.5, -0.5, 0.5,
			-0.5, -0.5, 0.5,
			-0.5,  0.5, 0.5,
			 0.5,  0.5, 0.5
		]);
		this.context.bufferData(this.context.ARRAY_BUFFER, positions, this.context.STATIC_DRAW);

		const positionLocation = this.context.getAttribLocation(this.program, "aVertexPosition");
		this.context.enableVertexAttribArray(positionLocation);
		this.context.vertexAttribPointer(positionLocation, 3, this.context.FLOAT, false, 0, 0);
	}
	createColors(){
		const colorBuffer = this.context.createBuffer();
		this.context.bindBuffer(this.context.ARRAY_BUFFER, colorBuffer);

		const colors = new Float32Array([
			1.0, 0.0, 0.0,
			1.0, 0.0, 0.0,
			1.0, 0.0, 0.0,
			1.0, 0.0, 0.0,

			0.0, 1.0, 0.0,
			0.0, 1.0, 0.0,
			0.0, 1.0, 0.0,
			0.0, 1.0, 0.0
		]);
		this.context.bufferData(this.context.ARRAY_BUFFER, colors, this.context.STATIC_DRAW);

		const vertexColorLocation = this.context.getAttribLocation(this.program, "aVertexColor");
		this.context.enableVertexAttribArray(vertexColorLocation);
		this.context.vertexAttribPointer(vertexColorLocation, 3, this.context.FLOAT, false, 0, 0);
	}
	createIndices() {
		const indexBuffer = this.context.createBuffer();
		this.context.bindBuffer(this.context.ELEMENT_ARRAY_BUFFER, indexBuffer);
		const indices = new Uint16Array([
			0, 1, 2, //front
			0, 2, 3,
			1, 4, 7, //right
			1, 7, 2,
			4, 5, 6, //back
			4, 6, 7,
			5, 0, 3, //left
			5, 3, 6,
			3, 2, 7, //top
			3, 7, 6,
			0, 1, 5, //bottom
			1, 4, 5
		]);
		this.context.bufferData(this.context.ELEMENT_ARRAY_BUFFER, indices, this.context.STATIC_DRAW);
	}
	setupUniforms(){
		const projectionMatrix = new Float32Array(getProjectionMatrix(this.#height, this.#width, 90, 0.01, 100));
		const projectionLocation = this.context.getUniformLocation(this.program, "uProjectionMatrix");
		this.context.uniformMatrix4fv(projectionLocation, false, projectionMatrix);
	}
	render() {
		this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT);
		this.context.drawElements(this.context.TRIANGLES, 24, this.context.UNSIGNED_SHORT, 0);
	}
	attributeChangedCallback(name, oldValue, newValue) {
		if (newValue !== oldValue) {
			this[name] = newValue;
		}
	}
	set height(value) {
		this.#height = value;
		if (this.dom) {
			this.dom.canvas.height = value;
		}
	}
	set width(value) {
		this.#width = value;
		if (this.dom) {
			this.dom.canvas.height = value;
		}
	}
	set image(value) {
		this.#image = value;
		loadImage(value)
			.then(img => this.createTexture(img));
	}
	//TODO: throw away program on detach
}

customElements.define("wc-geo-gl", WcGeoGl);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.