initial commit

This commit is contained in:
armel committed 2026-09-07 20:37:40 +02:00
commit e8c3f9862a
193 files changed
+29795

No files matched your search

+251
View File
@@ -0,0 +1,251 @@
import { Texture } from './Texture.js';
const VERTEX_SOURCE = `#version 300 es
layout(location = 0) in vec2 aPosition;
uniform mat4 uProjection;
uniform mat4 uModel;
out vec2 vTexCoord;
void main() {
vTexCoord = aPosition;
gl_Position = uProjection * uModel * vec4(aPosition, 0.0, 1.0);
}
`;
const FRAGMENT_SOURCE = `#version 300 es
precision mediump float;
in vec2 vTexCoord;
out vec4 outColor;
uniform sampler2D uSampler;
uniform vec4 uColor;
void main() {
outColor = texture(uSampler, vTexCoord) * uColor;
}
`;
const QUAD_POSITIONS = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
const QUAD_INDICES = new Uint16Array([0, 1, 2, 2, 1, 3]);
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const message = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error(`Shader compile error: ${message}`);
}
return shader;
}
export class Renderer {
constructor(canvas) {
this.canvas = canvas;
const gl = canvas.getContext('webgl2', {
alpha: true,
premultipliedAlpha: false,
antialias: true,
});
if (!gl) {
throw new Error('WebGL2 is not supported by this browser.');
}
this.gl = gl;
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SOURCE);
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SOURCE);
this.program = gl.createProgram();
gl.attachShader(this.program, vertexShader);
gl.attachShader(this.program, fragmentShader);
gl.linkProgram(this.program);
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
const message = gl.getProgramInfoLog(this.program);
throw new Error(`Program link error: ${message}`);
}
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
this.projectionLocation = gl.getUniformLocation(this.program, 'uProjection');
this.modelLocation = gl.getUniformLocation(this.program, 'uModel');
this.colorLocation = gl.getUniformLocation(this.program, 'uColor');
this.samplerLocation = gl.getUniformLocation(this.program, 'uSampler');
this._centeredProjection = new Float32Array(16);
this._modelMatrix = new Float32Array(16);
this._setupQuad();
this.resize(canvas.width, canvas.height);
gl.enable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
this.setBlendMode('normal');
gl.useProgram(this.program);
gl.uniform1i(this.samplerLocation, 0);
}
setBlendMode(mode) {
this._blendMode = mode;
const { gl } = this;
if (mode === 'additive') {
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
} else {
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
}
}
clear(r = 0, g = 0, b = 0, a = 0) {
const { gl } = this;
gl.clearColor(r, g, b, a);
gl.clear(gl.COLOR_BUFFER_BIT);
}
createTextureFromImage(image) {
const texture = new Texture(this.gl, image.width, image.height);
texture.uploadImage(image);
return texture;
}
get whiteTexture() {
if (!this._whiteTexture) {
this._whiteTexture = new Texture(this.gl, 1, 1);
}
return this._whiteTexture;
}
get glowTexture() {
if (!this._glowTexture) {
this._glowTexture = this._buildRadialTexture(64, (ctx, size) => {
const gradient = ctx.createRadialGradient(
size / 2, size / 2, 0,
size / 2, size / 2, size / 2,
);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(0.35, 'rgba(255, 255, 255, 0.6)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size, size);
});
}
return this._glowTexture;
}
get ringTexture() {
if (!this._ringTexture) {
this._ringTexture = this._buildRadialTexture(128, (ctx, size) => {
ctx.clearRect(0, 0, size, size);
ctx.lineWidth = size * 0.06;
ctx.strokeStyle = '#fff';
ctx.beginPath();
ctx.arc(size / 2, size / 2, size * 0.42, 0, Math.PI * 2);
ctx.stroke();
const gradient = ctx.createRadialGradient(
size / 2, size / 2, size * 0.28,
size / 2, size / 2, size * 0.52,
);
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.5)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(size / 2, size / 2, size * 0.5, 0, Math.PI * 2);
ctx.fill();
});
}
return this._ringTexture;
}
_buildRadialTexture(size, draw) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
draw(ctx, size);
const texture = new Texture(this.gl, size, size);
texture.uploadCanvas(canvas);
return texture;
}
drawTexture(texture, x, y, width, height, color = [1, 1, 1, 1], angle = 0) {
const { gl } = this;
this._setModelMatrix(x, y, width, height, angle);
gl.useProgram(this.program);
gl.uniformMatrix4fv(this.projectionLocation, false, this._centeredProjection);
gl.uniformMatrix4fv(this.modelLocation, false, this._modelMatrix);
gl.uniform4f(this.colorLocation, ...color);
gl.bindTexture(gl.TEXTURE_2D, texture.handle);
gl.bindVertexArray(this._quadVao);
gl.drawElements(gl.TRIANGLES, QUAD_INDICES.length, gl.UNSIGNED_SHORT, 0);
gl.bindVertexArray(null);
gl.bindTexture(gl.TEXTURE_2D, null);
}
resize(deviceWidth, deviceHeight, worldWidth = deviceWidth, worldHeight = deviceHeight) {
this.canvas.width = deviceWidth;
this.canvas.height = deviceHeight;
this.gl.viewport(0, 0, deviceWidth, deviceHeight);
this._worldWidth = worldWidth;
this._worldHeight = worldHeight;
this._syncProjection(0, 0, worldWidth, worldHeight);
}
setViewOffset(dx = 0, dy = 0) {
this._syncProjection(dx, dy, this._worldWidth, this._worldHeight);
}
_setupQuad() {
const { gl } = this;
this._quadVao = gl.createVertexArray();
gl.bindVertexArray(this._quadVao);
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, QUAD_POSITIONS, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, QUAD_INDICES, gl.STATIC_DRAW);
gl.bindVertexArray(null);
}
_setModelMatrix(x, y, width, height, angle = 0) {
const m = this._modelMatrix;
if (angle === 0) {
m[0] = width; m[1] = 0; m[2] = 0; m[3] = 0;
m[4] = 0; m[5] = height; m[6] = 0; m[7] = 0;
m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0;
m[12] = x; m[13] = y; m[14] = 0; m[15] = 1;
return;
}
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const cx = x + width / 2;
const cy = y + height / 2;
m[0] = width * cos; m[1] = width * sin; m[2] = 0; m[3] = 0;
m[4] = -height * sin; m[5] = height * cos; m[6] = 0; m[7] = 0;
m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0;
m[12] = cx; m[13] = cy; m[14] = 0; m[15] = 1;
}
_syncProjection(left, top, width, height) {
const m = this._centeredProjection;
m[0] = 2 / width; m[1] = 0; m[2] = 0; m[3] = 0;
m[4] = 0; m[5] = -2 / height; m[6] = 0; m[7] = 0;
m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0;
m[12] = -1 - (2 * left) / width; m[13] = 1 + (2 * top) / height; m[14] = 0; m[15] = 1;
}
}