251 lines
7.5 KiB
JavaScript
251 lines
7.5 KiB
JavaScript
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;
|
|
}
|
|
} |