<wc-geo-gl></wc-geo-gl>
const cube = {
	positions: [
		//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
	],
	colors: [
		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
	],
	triangles: [
		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, 4, 1, //bottom
		0, 5, 4
	]
};

const quadPyramid = {
	positions: [
		0.0, 0.5, 0.0,
		-0.5, -0.5, -0.5,
		0.5, -0.5, -0.5,
		0.5, -0.5, 0.5,
		-0.5, -0.5, 0.5 
	],
	colors: [
		1.0, 0, 0,
		0, 0, 1,
		0, 0, 1,
		0, 0, 1,
		0, 0, 1
	],
	triangles: [
		0, 1, 2,
		0, 2, 3,
		0, 3, 4,
		0, 4, 1
	]
}

class Mesh {
	#positions;
	#colors;
	#normals;
	#uvs;
	#triangles;

	#translation = [0, 0, 0];
	#scale = [1, 1, 1];
	#rotation = [0, 0, 0];

	constructor(mesh){
		this.positions = mesh.positions;
		this.colors = mesh.colors;
		this.normals = mesh.normals;
		this.uvs = mesh.uvs;
		this.triangles = mesh.triangles;
	}

	set positions(val){
		this.#positions = new Float32Array(val);
	}
	get positions(){
		return this.#positions;
	}
	set colors(val) {
		this.#colors = new Float32Array(val);
	}
	get colors(){
		return this.#colors;
	}
	set normals(val) {
		this.#normals = new Float32Array(val);
	}
	get normals(){
		return this.#normals;
	}
	set uvs(val) {
		this.#uvs = new Float32Array(val);
	}
	get uvs(){
		return this.#uvs;
	}
	set triangles(val) {
		this.#triangles = new Uint16Array(val);
	}
	get triangles(){
		return this.#triangles;
	}
	setTranslation({ x, y, z }){
		if (x){
			this.#translation[0] = x;
		}
		if (y) {
			this.#translation[1] = y;
		}
		if (z) {
			this.#translation[2] = z;
		}
	}
	getTranslation(){
		return this.#translation;
	}
	setScale({ x, y, z }) {
		if (x) {
			this.#scale[0] = x;
		}
		if (y) {
			this.#scale[1] = y;
		}
		if (z) {
			this.#scale[2] = z;
		}
	}
	getScale() {
		return this.#scale;
	}
	setRotation({ x, y, z }) {
		if (x) {
			this.#rotation[0] = x;
		}
		if (y) {
			this.#rotation[1] = y;
		}
		if (z) {
			this.#rotation[2] = z;
		}
	}
	getRotation(){
		return this.#rotation;
	}
	getModelMatrix(){
		return new Float32Array(transpose(multiplyMatrix(
			getTranslationMatrix(this.#translation[0], this.#translation[1], this.#translation[2]),
				multiplyMatrix(
					getRotationXMatrix(this.#rotation[0]),
					multiplyMatrix(
						getRotationYMatrix(this.#rotation[1]),
						multiplyMatrix(
							getRotationZMatrix(this.#rotation[2]),
							multiplyMatrix(
								getScaleMatrix(this.#scale[0], this.#scale[1], this.#scale[2]),
								getIdentityMatrix()
							)
						)
					)
				)
			)).flat());
	}
}

function getRotationXMatrix(theta) {
	return [
		[1, 0, 0, 0],
		[0, Math.cos(theta), -Math.sin(theta), 0],
		[0, Math.sin(theta), Math.cos(theta), 0],
		[0, 0, 0, 1]
	];
}

function getRotationYMatrix(theta) {
	return [
		[Math.cos(theta), 0, Math.sin(theta), 0],
		[0, 1, 0, 0],
		[-Math.sin(theta), 0, Math.cos(theta), 0],
		[0, 0, 0, 1]
	];
}

function getRotationZMatrix(theta) {
	return [
		[Math.cos(theta), -Math.sin(theta), 0, 0],
		[Math.sin(theta), Math.cos(theta), 0, 0],
		[0, 0, 1, 0],
		[0, 0, 0, 1]
	];
}

function getTranslationMatrix(x, y, z) {
	return [
		[1, 0, 0, x],
		[0, 1, 0, y],
		[0, 0, 1, z],
		[0, 0, 0, 1]
	];
}

function getScaleMatrix(x, y, z){
	return [
		[x, 0, 0, 0],
		[0, y, 0, 0],
		[0, 0, z, 0],
		[0, 0, 0, 1]
	];
}

function multiplyMatrix(a, b) {
	const matrix = [
		new Array(4),
		new Array(4),
		new Array(4),
		new Array(4)
	];
	for (let c = 0; c < 4; c++) {
		for (let r = 0; r < 4; r++) {
			matrix[r][c] = a[r][0] * b[0][c] + a[r][1] * b[1][c] + a[r][2] * b[2][c] + a[r][3] * b[3][c];
		}
	}

	return matrix;
}

function getIdentityMatrix() {
	return [
		[1, 0, 0, 0],
		[0, 1, 0, 0],
		[0, 0, 1, 0],
		[0, 0, 0, 1]
	];
}

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 transpose(matrix){
	return [
		[matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0]],
		[matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1]],
		[matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2]],
		[matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3]]
	];
}

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.createMeshes();
		this.setupGlobalUniforms();
		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;
				uniform mat4 uModelMatrix;
				
				attribute vec3 aVertexPosition;
				attribute vec3 aVertexColor;

				varying mediump vec4 vColor;

				void main(){
					gl_Position = uProjectionMatrix * uModelMatrix * 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.enable(this.context.CULL_FACE);
		this.context.cullFace(this.context.BACK);
		this.context.useProgram(this.program);
	}

	createMeshes(){
		const tcube = new Mesh(cube);
		tcube.setRotation({ x: Math.PI / 4, y: Math.PI / 4 });
		tcube.setTranslation({ z: 2, x: 0.75 });

		const tpyramid = new Mesh(quadPyramid);
		tpyramid.setTranslation({ z: 2, x: -0.75 });

		this.meshes = {
			pyramid: tpyramid,
			cube: tcube
		};
	}

	bindMesh(mesh){
		this.bindPositions(mesh.positions);
		this.bindColors(mesh.colors);
		this.bindIndices(mesh.triangles);
		this.bindUniforms(mesh.getModelMatrix());
	}

	bindPositions(positions) {
		const positionBuffer = this.context.createBuffer();
		this.context.bindBuffer(this.context.ARRAY_BUFFER, positionBuffer);

		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);
	}
	bindColors(colors){
		const colorBuffer = this.context.createBuffer();
		this.context.bindBuffer(this.context.ARRAY_BUFFER, colorBuffer);

		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);
	}
	bindIndices(indices) {
		const indexBuffer = this.context.createBuffer();
		this.context.bindBuffer(this.context.ELEMENT_ARRAY_BUFFER, indexBuffer);
		this.context.bufferData(this.context.ELEMENT_ARRAY_BUFFER, indices, this.context.STATIC_DRAW);
	}
	bindUniforms(modelMatrix){
		const modelMatrixLocation = this.context.getUniformLocation(this.program, "uModelMatrix");
		this.context.uniformMatrix4fv(modelMatrixLocation, false, modelMatrix);
	}
	setupGlobalUniforms(){
		const projectionMatrix = new Float32Array(getProjectionMatrix(this.#height, this.#width, 90, 0.01, 100).flat());
		const projectionLocation = this.context.getUniformLocation(this.program, "uProjectionMatrix");
		this.context.uniformMatrix4fv(projectionLocation, false, projectionMatrix);
	}
	createTexture(image) {
		const texture = this.context.createTexture();
		this.context.bindTexture(this.context.TEXTURE_2D, texture);

		this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_WRAP_S, this.context.CLAMP_TO_EDGE);
		this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_WRAP_T, this.context.CLAMP_TO_EDGE);
		this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_MIN_FILTER, this.context.NEAREST);
		this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_MAG_FILTER, this.context.NEAREST);

		this.context.texImage2D(this.context.TEXTURE_2D, 24, this.context.RGBA, this.context.RGBA, this.context.UNSIGNED_BYTE, image);
	}
	render() {
		this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT);
		for (const mesh of Object.values(this.meshes)){
			this.bindMesh(mesh);
			this.context.drawElements(this.context.TRIANGLES, mesh.triangles.length, 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.