885 lines
26 KiB
JavaScript
885 lines
26 KiB
JavaScript
/**
|
|
* MATRIX_WORKER.JS
|
|
* Архитектура: WebGL2 Instanced Rendering (Zero-Copy) + 2D Overlay
|
|
* Полностью собранный и оптимизированный файл
|
|
*/
|
|
|
|
let engine, wasm;
|
|
let gl;
|
|
let overlayCanvas, overlayCtx;
|
|
let atlasCanvas, atlasCtx, atlasTexture;
|
|
let width, height;
|
|
let fontSize = 16;
|
|
let isMobile = false,
|
|
isVpnOn = false,
|
|
isDarkMode = true;
|
|
let lastDrawTime = 0;
|
|
let pixelScale = 1.0;
|
|
let isLowQuality = false;
|
|
let frameCounter = 0;
|
|
let eyeScale = 1.5;
|
|
|
|
let postProgram;
|
|
let postFramebuffer, postTexture;
|
|
|
|
let MatrixEngineClass;
|
|
const workerUrlParams = new URLSearchParams(self.location.search);
|
|
const version = workerUrlParams.get("v") || Date.now().toString();
|
|
|
|
const chars =
|
|
"0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레로와원>_±÷×=≠≈≡≤≥";
|
|
|
|
// --- WEBGL SHADERS ---
|
|
const vsMatrix = `#version 300 es
|
|
precision mediump float;
|
|
|
|
in vec2 a_quad;
|
|
in vec2 a_position;
|
|
in float a_charIdx;
|
|
in float a_atlasRow;
|
|
in float a_scale;
|
|
|
|
uniform vec2 u_resolution;
|
|
uniform float u_fontSize;
|
|
|
|
out vec2 v_uv;
|
|
out float v_atlasRow;
|
|
|
|
void main() {
|
|
float size = u_fontSize * a_scale;
|
|
vec2 pos = a_position + a_quad * size;
|
|
vec2 zeroToOne = pos / u_resolution;
|
|
vec2 zeroToTwo = zeroToOne * 2.0;
|
|
vec2 clipSpace = zeroToTwo - 1.0;
|
|
gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);
|
|
v_uv = vec2(a_charIdx * u_fontSize + a_quad.x * u_fontSize, a_quad.y * u_fontSize);
|
|
v_atlasRow = a_atlasRow;
|
|
}
|
|
`.trim(); // <--- ВОТ ТУТ МАГИЯ
|
|
|
|
const fsMatrix = `#version 300 es
|
|
precision mediump float;
|
|
|
|
in vec2 v_uv;
|
|
in float v_atlasRow;
|
|
|
|
uniform sampler2D u_atlas;
|
|
uniform float u_fontSize;
|
|
uniform vec2 u_atlasSize;
|
|
|
|
out vec4 outColor;
|
|
|
|
void main() {
|
|
float yOffset = v_atlasRow * u_fontSize;
|
|
vec2 finalUV = vec2(v_uv.x, v_uv.y + yOffset) / u_atlasSize;
|
|
vec4 texColor = texture(u_atlas, finalUV);
|
|
if (texColor.a < 0.001) discard;
|
|
outColor = texColor;
|
|
}
|
|
`.trim();
|
|
|
|
const vsOverlay = `#version 300 es
|
|
precision mediump float;
|
|
|
|
in vec2 a_position;
|
|
out vec2 v_uv;
|
|
void main() {
|
|
v_uv = a_position * 0.5 + 0.5;
|
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
}
|
|
`.trim();
|
|
|
|
const fsOverlay = `#version 300 es
|
|
precision mediump float;
|
|
|
|
in vec2 v_uv;
|
|
uniform sampler2D u_texture;
|
|
out vec4 outColor;
|
|
void main() {
|
|
vec4 color = texture(u_texture, v_uv);
|
|
if (color.a < 0.01) discard;
|
|
outColor = color;
|
|
}
|
|
`.trim();
|
|
|
|
const postProcessSource = `#version 300 es
|
|
precision highp float;
|
|
|
|
in vec2 v_uv;
|
|
out vec4 outColor;
|
|
|
|
uniform sampler2D u_mainTex;
|
|
uniform float u_time;
|
|
uniform vec2 u_resolution;
|
|
uniform float u_distortion;
|
|
uniform float u_aberration;
|
|
uniform float u_scanlines;
|
|
uniform float u_noise;
|
|
uniform float u_glitch_intensity;
|
|
uniform float u_isDarkMode;
|
|
|
|
vec2 crt_coords(vec2 uv, float bend) {
|
|
uv = uv * 2.0 - 1.0;
|
|
vec2 offset = abs(uv.yx) / vec2(bend);
|
|
uv = uv + uv * offset * offset;
|
|
uv = uv * 0.5 + 0.5;
|
|
return uv;
|
|
}
|
|
|
|
float hash21(vec2 p) {
|
|
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
|
|
}
|
|
|
|
void main() {
|
|
vec2 crt_uv = crt_coords(v_uv, u_distortion);
|
|
|
|
if (crt_uv.x < 0.0 || crt_uv.x > 1.0 || crt_uv.y < 0.0 || crt_uv.y > 1.0) {
|
|
float bg = u_isDarkMode > 0.5 ? 0.0 : 0.98;
|
|
outColor = vec4(bg, bg, bg, 1.0);
|
|
return;
|
|
}
|
|
|
|
float glitch_trigger = step(1.0 - (0.02 * u_glitch_intensity), sin(u_time * 12.0 + crt_uv.y * 20.0)) * step(0.95, sin(u_time * 25.0));
|
|
float tear_offset = glitch_trigger * 0.05 * sin(u_time * 50.0) * u_glitch_intensity;
|
|
vec2 final_uv = crt_uv + vec2(tear_offset, 0.0);
|
|
|
|
float r_offset = u_aberration * (final_uv.x - 0.5);
|
|
float b_offset = -u_aberration * (final_uv.x - 0.5);
|
|
|
|
float r = texture(u_mainTex, final_uv + vec2(r_offset, 0.0)).r;
|
|
float g = texture(u_mainTex, final_uv).g;
|
|
float b = texture(u_mainTex, final_uv + vec2(b_offset, 0.0)).b;
|
|
vec4 base_color = vec4(r, g, b, 1.0);
|
|
|
|
if (glitch_trigger > 0.0) {
|
|
base_color.rgb = vec3(g, b, r) + (u_isDarkMode > 0.5 ? vec3(0.2) : vec3(-0.2));
|
|
}
|
|
|
|
float scanline = sin(final_uv.y * u_resolution.y * 3.1415) * 0.5 + 0.5;
|
|
float vignette = final_uv.x * final_uv.y * (1.0 - final_uv.x) * (1.0 - final_uv.y);
|
|
vignette = clamp(pow(20.0 * vignette, 0.3), 0.0, 1.0);
|
|
float noise_val = hash21(final_uv + u_time) * u_noise;
|
|
|
|
// --- РАЗДЕЛЕНИЕ ЛОГИКИ ТЕМ ---
|
|
if (u_isDarkMode > 0.5) {
|
|
base_color.rgb *= (1.0 - (scanline * u_scanlines));
|
|
base_color.rgb *= vignette;
|
|
base_color.rgb += noise_val;
|
|
base_color.rgb = smoothstep(0.02, 0.95, base_color.rgb);
|
|
base_color.rgb += pow(base_color.rgb, vec3(1.8)) * 0.25;
|
|
} else {
|
|
float is_text = 1.0 - smoothstep(0.8, 0.95, base_color.g);
|
|
base_color.rgb *= (1.0 - (scanline * u_scanlines * 0.3 * is_text));
|
|
base_color.rgb = mix(vec3(1.0), base_color.rgb, vignette);
|
|
base_color.rgb -= noise_val * 0.2;
|
|
base_color.rgb = smoothstep(0.0, 0.98, base_color.rgb);
|
|
}
|
|
|
|
outColor = base_color;
|
|
}
|
|
`.trim();
|
|
|
|
// --- WEBGL STATE ---
|
|
let matrixProgram, overlayProgram;
|
|
let instanceBuffer, quadBuffer, overlayQuadBuffer;
|
|
let vaoMatrix, vaoOverlay;
|
|
let uResLoc, uFontLoc, uAtlasSizeLoc;
|
|
let overlayTexture;
|
|
let instanceBufferCapacity = 0;
|
|
|
|
// --- ИНИЦИАЛИЗАЦИЯ WEBGL ---
|
|
function compileShader(type, source) {
|
|
const shader = gl.createShader(type);
|
|
gl.shaderSource(shader, source);
|
|
gl.compileShader(shader);
|
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
console.error(gl.getShaderInfoLog(shader));
|
|
gl.deleteShader(shader);
|
|
return null;
|
|
}
|
|
return shader;
|
|
}
|
|
|
|
function createProgram(vsSource, fsSource) {
|
|
const vs = compileShader(gl.VERTEX_SHADER, vsSource);
|
|
const fs = compileShader(gl.FRAGMENT_SHADER, fsSource);
|
|
const prog = gl.createProgram();
|
|
gl.attachShader(prog, vs);
|
|
gl.attachShader(prog, fs);
|
|
gl.linkProgram(prog);
|
|
return prog;
|
|
}
|
|
|
|
function initWebGL() {
|
|
matrixProgram = createProgram(vsMatrix, fsMatrix);
|
|
overlayProgram = createProgram(vsOverlay, fsOverlay);
|
|
|
|
// --- VAO для Матрицы ---
|
|
vaoMatrix = gl.createVertexArray();
|
|
gl.bindVertexArray(vaoMatrix);
|
|
|
|
quadBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
gl.bufferData(
|
|
gl.ARRAY_BUFFER,
|
|
new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]),
|
|
gl.STATIC_DRAW,
|
|
);
|
|
|
|
const aQuadLoc = gl.getAttribLocation(matrixProgram, "a_quad");
|
|
gl.enableVertexAttribArray(aQuadLoc);
|
|
gl.vertexAttribPointer(aQuadLoc, 2, gl.FLOAT, false, 0, 0);
|
|
gl.vertexAttribDivisor(aQuadLoc, 0);
|
|
|
|
instanceBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
|
|
instanceBufferCapacity = 20000;
|
|
gl.bufferData(gl.ARRAY_BUFFER, instanceBufferCapacity * 4, gl.DYNAMIC_DRAW);
|
|
|
|
const strideBytes = 5 * 4;
|
|
const aPosLoc = gl.getAttribLocation(matrixProgram, "a_position");
|
|
gl.enableVertexAttribArray(aPosLoc);
|
|
gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, strideBytes, 0);
|
|
gl.vertexAttribDivisor(aPosLoc, 1);
|
|
|
|
const aCharIdxLoc = gl.getAttribLocation(matrixProgram, "a_charIdx");
|
|
gl.enableVertexAttribArray(aCharIdxLoc);
|
|
gl.vertexAttribPointer(aCharIdxLoc, 1, gl.FLOAT, false, strideBytes, 2 * 4);
|
|
gl.vertexAttribDivisor(aCharIdxLoc, 1);
|
|
|
|
const aAtlasRowLoc = gl.getAttribLocation(matrixProgram, "a_atlasRow");
|
|
gl.enableVertexAttribArray(aAtlasRowLoc);
|
|
gl.vertexAttribPointer(aAtlasRowLoc, 1, gl.FLOAT, false, strideBytes, 3 * 4);
|
|
gl.vertexAttribDivisor(aAtlasRowLoc, 1);
|
|
|
|
const aScaleLoc = gl.getAttribLocation(matrixProgram, "a_scale");
|
|
gl.enableVertexAttribArray(aScaleLoc);
|
|
gl.vertexAttribPointer(aScaleLoc, 1, gl.FLOAT, false, strideBytes, 4 * 4);
|
|
gl.vertexAttribDivisor(aScaleLoc, 1);
|
|
|
|
gl.bindVertexArray(null);
|
|
|
|
// --- VAO для Overlay ---
|
|
vaoOverlay = gl.createVertexArray();
|
|
gl.bindVertexArray(vaoOverlay);
|
|
|
|
overlayQuadBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, overlayQuadBuffer);
|
|
gl.bufferData(
|
|
gl.ARRAY_BUFFER,
|
|
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
|
|
gl.STATIC_DRAW,
|
|
);
|
|
|
|
const aPosLocOver = gl.getAttribLocation(overlayProgram, "a_position");
|
|
gl.enableVertexAttribArray(aPosLocOver);
|
|
gl.vertexAttribPointer(aPosLocOver, 2, gl.FLOAT, false, 0, 0);
|
|
gl.vertexAttribDivisor(aPosLocOver, 0);
|
|
|
|
gl.bindVertexArray(null);
|
|
|
|
// --- Uniforms & Textures ---
|
|
uResLoc = gl.getUniformLocation(matrixProgram, "u_resolution");
|
|
uFontLoc = gl.getUniformLocation(matrixProgram, "u_fontSize");
|
|
uAtlasSizeLoc = gl.getUniformLocation(matrixProgram, "u_atlasSize");
|
|
|
|
atlasTexture = gl.createTexture();
|
|
overlayTexture = gl.createTexture();
|
|
|
|
// ИНИЦИАЛИЗАЦИЯ ПОСТОБРАБОТКИ
|
|
postProgram = createProgram(vsOverlay, postProcessSource);
|
|
postTexture = gl.createTexture();
|
|
postFramebuffer = gl.createFramebuffer();
|
|
|
|
// Настройка текстуры постобработки (размер установим в resize)
|
|
gl.bindTexture(gl.TEXTURE_2D, postTexture);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
|
|
// Смена настроек WebGL
|
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
gl.enable(gl.BLEND);
|
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
}
|
|
|
|
function buildAtlas() {
|
|
atlasCanvas = new OffscreenCanvas(chars.length * fontSize, fontSize * 11);
|
|
atlasCtx = atlasCanvas.getContext("2d", { alpha: true });
|
|
atlasCtx.clearRect(0, 0, atlasCanvas.width, atlasCanvas.height);
|
|
|
|
let r, g, b;
|
|
if (isVpnOn && isDarkMode) {
|
|
r = 139;
|
|
g = 61;
|
|
b = 255;
|
|
} else if (!isVpnOn && isDarkMode) {
|
|
r = 0;
|
|
g = 229;
|
|
b = 242;
|
|
} else if (isVpnOn && !isDarkMode) {
|
|
r = 109;
|
|
g = 40;
|
|
b = 217;
|
|
} else {
|
|
r = 2;
|
|
g = 132;
|
|
b = 199;
|
|
}
|
|
|
|
atlasCtx.font = `bold ${fontSize}px monospace`;
|
|
atlasCtx.textBaseline = "top";
|
|
atlasCtx.textAlign = "center";
|
|
const halfFs = fontSize / 2;
|
|
|
|
for (let row = 0; row <= 9; row++) {
|
|
const alpha = (row + 1) / 10;
|
|
const yOffset = row * fontSize;
|
|
|
|
// УБРАЛИ fillRect с серым фоном. Оставляем ТОЛЬКО цветной текст!
|
|
atlasCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
for (let i = 0; i < chars.length; i++) {
|
|
atlasCtx.fillText(chars[i], i * fontSize + halfFs, yOffset);
|
|
}
|
|
}
|
|
|
|
const headY = 10 * fontSize;
|
|
// УБРАЛИ fillRect для "головы"
|
|
atlasCtx.fillStyle = "rgb(255, 255, 255)";
|
|
for (let i = 0; i < chars.length; i++) {
|
|
atlasCtx.fillText(chars[i], i * fontSize + halfFs, headY);
|
|
}
|
|
|
|
if (gl) {
|
|
gl.bindTexture(gl.TEXTURE_2D, atlasTexture);
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RGBA,
|
|
gl.RGBA,
|
|
gl.UNSIGNED_BYTE,
|
|
atlasCanvas,
|
|
);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
}
|
|
}
|
|
|
|
// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ОТРИСОВКИ ---
|
|
function getGlitchColor(type) {
|
|
let r, g, b;
|
|
if (isVpnOn && isDarkMode) {
|
|
r = 139;
|
|
g = 61;
|
|
b = 255;
|
|
} else if (!isVpnOn && isDarkMode) {
|
|
r = 0;
|
|
g = 229;
|
|
b = 242;
|
|
} else if (isVpnOn && !isDarkMode) {
|
|
r = 109;
|
|
g = 40;
|
|
b = 217;
|
|
} else {
|
|
r = 2;
|
|
g = 132;
|
|
b = 199;
|
|
}
|
|
|
|
if (type === 0) return isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
|
|
if (type === 1) return `rgba(${r},${g},${b},0.9)`;
|
|
if (type === 2) return `rgba(${255 - r},${255 - g},${255 - b},0.7)`;
|
|
if (type === 3) return "rgba(255,255,255,0.95)";
|
|
return "rgba(255,20,80,0.95)";
|
|
}
|
|
|
|
function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
|
|
const color = isVpnOn
|
|
? isDarkMode
|
|
? "rgb(139, 61, 255)"
|
|
: "rgb(109, 40, 217)"
|
|
: isDarkMode
|
|
? "rgb(0, 229, 242)"
|
|
: "rgb(2, 132, 199)";
|
|
|
|
const bgFill = isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
|
|
|
|
const eyeHeight = (72 * (1 - isClosed) + 6 * isClosed) * eyeScale;
|
|
const eyeWidth = (120 * (1 - isClosed) + 140 * isClosed) * eyeScale;
|
|
const eyeRadius = (36 * (1 - isClosed) + 8 * isClosed) * eyeScale;
|
|
const laserWidth = 140 * isClosed * eyeScale;
|
|
const blurMultiplier = isMobile ? 0 : isDarkMode ? 1.0 : 0.1;
|
|
|
|
ctx.save();
|
|
ctx.fillStyle = bgFill;
|
|
ctx.beginPath();
|
|
ctx.roundRect(
|
|
cx - eyeWidth / 2,
|
|
cy - eyeHeight / 2,
|
|
eyeWidth,
|
|
eyeHeight,
|
|
eyeRadius,
|
|
);
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 3.5 * (1 - isClosed) + 2.5 * isClosed;
|
|
ctx.shadowColor = color;
|
|
ctx.shadowBlur = (12 * (1 - isClosed) + 6 * isClosed) * blurMultiplier;
|
|
ctx.stroke();
|
|
|
|
if (isVpnOn && isClosed > 0.1) {
|
|
ctx.fillStyle = color;
|
|
ctx.shadowColor = color;
|
|
ctx.shadowBlur = 8 * blurMultiplier;
|
|
ctx.globalAlpha = isClosed;
|
|
ctx.fillRect(cx - laserWidth / 2, cy - 1, laserWidth, 2);
|
|
}
|
|
|
|
if (isClosed < 0.99) {
|
|
const alphaMult = 1 - isClosed;
|
|
ctx.save();
|
|
ctx.translate(cx + gazeX * eyeScale, cy + gazeY * eyeScale);
|
|
|
|
const t = performance.now() * 0.003;
|
|
|
|
ctx.save();
|
|
ctx.rotate(t);
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 1;
|
|
ctx.globalAlpha = 0.7 * alphaMult;
|
|
ctx.setLineDash([8 * eyeScale, 6 * eyeScale, 2 * eyeScale, 6 * eyeScale]);
|
|
ctx.shadowBlur = 0;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, 16 * eyeScale, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
|
|
// ИСПРАВЛЕНИЕ: Делаем зрачок и перекрестие ЖЕСТКО черным в светлой теме
|
|
ctx.strokeStyle = isDarkMode ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.85)";
|
|
ctx.lineWidth = isDarkMode ? 1 : 1.5;
|
|
ctx.globalAlpha = alphaMult;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, 10 * eyeScale, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
const pulse = Math.sin(t * 5) * 0.15 + 0.85;
|
|
ctx.save();
|
|
ctx.scale(pulse, pulse);
|
|
|
|
ctx.fillStyle = isDarkMode ? "#fff" : "#000";
|
|
ctx.shadowBlur = 10 * blurMultiplier;
|
|
ctx.shadowColor = color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, -6 * eyeScale);
|
|
ctx.lineTo(5 * eyeScale, 0);
|
|
ctx.lineTo(0, 6 * eyeScale);
|
|
ctx.lineTo(-5 * eyeScale, 0);
|
|
ctx.fill();
|
|
|
|
// Центральная точка
|
|
ctx.fillStyle = isDarkMode ? color : "rgba(0,0,0,0.9)";
|
|
ctx.shadowBlur = 0;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, 1.5 * eyeScale, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 1;
|
|
ctx.shadowBlur = 5 * blurMultiplier;
|
|
ctx.globalAlpha = 0.4 * alphaMult;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-22 * eyeScale, 0);
|
|
ctx.lineTo(-12 * eyeScale, 0);
|
|
ctx.moveTo(22 * eyeScale, 0);
|
|
ctx.lineTo(12 * eyeScale, 0);
|
|
ctx.moveTo(0, -22 * eyeScale);
|
|
ctx.lineTo(0, -12 * eyeScale);
|
|
ctx.moveTo(0, 22 * eyeScale);
|
|
ctx.lineTo(0, 12 * eyeScale);
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = color;
|
|
ctx.font = `${Math.max(8, Math.round(10 * eyeScale))}px monospace`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.shadowBlur = 8 * blurMultiplier;
|
|
|
|
ctx.globalAlpha = (dir === 1 ? 1.0 : 0.15) * alphaMult;
|
|
ctx.fillText("▴", 0, -26 * eyeScale);
|
|
ctx.globalAlpha = (dir === 2 ? 1.0 : 0.15) * alphaMult;
|
|
ctx.fillText("▾", 0, 26 * eyeScale);
|
|
ctx.globalAlpha = (dir === 3 ? 1.0 : 0.15) * alphaMult;
|
|
ctx.fillText("◂", -26 * eyeScale, 1);
|
|
ctx.globalAlpha = (dir === 4 ? 1.0 : 0.15) * alphaMult;
|
|
ctx.fillText("▸", 26 * eyeScale, 1);
|
|
|
|
ctx.restore();
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawOverlayElements() {
|
|
overlayCtx.clearRect(0, 0, width, height);
|
|
let hasContent = false;
|
|
|
|
const glLen = engine.glitch_len();
|
|
if (glLen > 0) {
|
|
hasContent = true;
|
|
const glData = new Float32Array(
|
|
wasm.memory.buffer,
|
|
engine.glitch_ptr(),
|
|
glLen,
|
|
);
|
|
for (let i = 0; i < glLen; i += 5) {
|
|
overlayCtx.fillStyle = getGlitchColor(glData[i + 4]);
|
|
overlayCtx.fillRect(
|
|
glData[i],
|
|
glData[i + 1],
|
|
glData[i + 2],
|
|
glData[i + 3],
|
|
);
|
|
}
|
|
}
|
|
|
|
const gsLen = engine.glass_len();
|
|
if (gsLen > 0) {
|
|
hasContent = true;
|
|
const gsData = new Float32Array(
|
|
wasm.memory.buffer,
|
|
engine.glass_ptr(),
|
|
gsLen,
|
|
);
|
|
for (let i = 0; i < gsLen; i += 4) {
|
|
overlayCtx.globalAlpha = gsData[i + 3];
|
|
overlayCtx.drawImage(
|
|
atlasCanvas,
|
|
gsData[i + 2] * fontSize,
|
|
0,
|
|
fontSize,
|
|
fontSize,
|
|
gsData[i],
|
|
gsData[i + 1],
|
|
fontSize,
|
|
fontSize,
|
|
);
|
|
}
|
|
}
|
|
|
|
const eyeLen = engine.eye_len();
|
|
if (eyeLen >= 6) {
|
|
hasContent = true;
|
|
overlayCtx.globalAlpha = 1.0;
|
|
const eyeData = new Float32Array(
|
|
wasm.memory.buffer,
|
|
engine.eye_ptr(),
|
|
eyeLen,
|
|
);
|
|
const gap = isMobile ? 85 * eyeScale : 85 * eyeScale;
|
|
drawEye(
|
|
overlayCtx,
|
|
eyeData[0] - gap,
|
|
eyeData[1],
|
|
eyeData[2],
|
|
eyeData[3],
|
|
eyeData[4],
|
|
eyeData[5],
|
|
);
|
|
drawEye(
|
|
overlayCtx,
|
|
eyeData[0] + gap,
|
|
eyeData[1],
|
|
eyeData[2],
|
|
eyeData[3],
|
|
eyeData[4],
|
|
eyeData[5],
|
|
);
|
|
}
|
|
|
|
return hasContent;
|
|
}
|
|
// --- ГЛАВНЫЙ ЦИКЛ ОТРИСОВКИ ---
|
|
function render(currentTime) {
|
|
requestAnimationFrame(render);
|
|
if (!engine || !wasm || !gl) return;
|
|
|
|
const now = currentTime || performance.now();
|
|
if (now - lastDrawTime < 16) return;
|
|
lastDrawTime = now;
|
|
|
|
if (isLowQuality) {
|
|
frameCounter++;
|
|
if (frameCounter % 2 === 0) {
|
|
engine.tick();
|
|
}
|
|
} else {
|
|
engine.tick();
|
|
}
|
|
|
|
// --- ШАГ 1: РЕНДЕРИМ ВСЁ В ТЕКСТУРУ (FRAMEBUFFER) ---
|
|
// Направляем вывод WebGL в буфер постобработки, а не на экран
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, postFramebuffer);
|
|
gl.viewport(0, 0, width * pixelScale, height * pixelScale);
|
|
|
|
const bgR = isDarkMode ? 10 / 255 : 250 / 255;
|
|
const bgG = isDarkMode ? 10 / 255 : 250 / 255;
|
|
const bgB = isDarkMode ? 12 / 255 : 252 / 255;
|
|
gl.clearColor(bgR, bgG, bgB, 1.0);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
|
|
// 1.1. РЕНДЕР МАТРИЦЫ
|
|
const renderLen = engine.render_len();
|
|
const instanceCount = renderLen / 5;
|
|
if (instanceCount > 0) {
|
|
gl.useProgram(matrixProgram);
|
|
gl.bindVertexArray(vaoMatrix);
|
|
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, atlasTexture);
|
|
gl.uniform1i(gl.getUniformLocation(matrixProgram, "u_atlas"), 0);
|
|
gl.uniform2f(uResLoc, width, height);
|
|
gl.uniform1f(uFontLoc, fontSize);
|
|
gl.uniform2f(uAtlasSizeLoc, atlasCanvas.width, atlasCanvas.height);
|
|
|
|
const renderDataView = new Float32Array(
|
|
wasm.memory.buffer,
|
|
engine.render_ptr(),
|
|
renderLen,
|
|
);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
|
|
if (renderLen > instanceBufferCapacity) {
|
|
instanceBufferCapacity = Math.max(renderLen * 2, 20000);
|
|
gl.bufferData(
|
|
gl.ARRAY_BUFFER,
|
|
instanceBufferCapacity * 4,
|
|
gl.DYNAMIC_DRAW,
|
|
);
|
|
}
|
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, renderDataView);
|
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount);
|
|
}
|
|
|
|
// 1.2. РЕНДЕР ОВЕРЛЕЯ (Глаза, глитчи)
|
|
if (drawOverlayElements()) {
|
|
gl.useProgram(overlayProgram);
|
|
gl.bindVertexArray(vaoOverlay);
|
|
gl.activeTexture(gl.TEXTURE1);
|
|
gl.bindTexture(gl.TEXTURE_2D, overlayTexture);
|
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RGBA,
|
|
gl.RGBA,
|
|
gl.UNSIGNED_BYTE,
|
|
overlayCanvas,
|
|
);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
gl.uniform1i(gl.getUniformLocation(overlayProgram, "u_texture"), 1);
|
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
}
|
|
|
|
// --- ШАГ 2: ПОСТОБРАБОТКА (ВЫВОД НА ЭКРАН) ---
|
|
// Возвращаемся к стандартному буферу (экран)
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
|
|
gl.useProgram(postProgram); // <--- ИСПОЛЬЗУЕМ ВАШ НОВЫЙ ШЕЙДЕР
|
|
gl.bindVertexArray(vaoOverlay); // Переиспользуем квадрат на весь экран
|
|
|
|
// Передаем текстуру, которую только что нарисовали (Шаг 1)
|
|
gl.activeTexture(gl.TEXTURE2);
|
|
gl.bindTexture(gl.TEXTURE_2D, postTexture);
|
|
gl.uniform1i(gl.getUniformLocation(postProgram, "u_mainTex"), 2);
|
|
|
|
// Передаем параметры эффектов
|
|
gl.uniform1f(gl.getUniformLocation(postProgram, "u_time"), now / 1000);
|
|
gl.uniform2f(
|
|
gl.getUniformLocation(postProgram, "u_resolution"),
|
|
width * pixelScale,
|
|
height * pixelScale,
|
|
);
|
|
|
|
// Значения: Сбалансированный киберпанк
|
|
gl.uniform1f(gl.getUniformLocation(postProgram, "u_distortion"), 18.0); // Изгиб оставляем как есть
|
|
gl.uniform1f(gl.getUniformLocation(postProgram, "u_aberration"), 0.008); // Уменьшили "радугу" по краям в два раза
|
|
gl.uniform1f(gl.getUniformLocation(postProgram, "u_scanlines"), 0.15); // Сделали полосы тоньше и прозрачнее
|
|
gl.uniform1f(gl.getUniformLocation(postProgram, "u_noise"), 0.05); // Чуть убавили цифровой шум
|
|
|
|
gl.uniform1f(
|
|
gl.getUniformLocation(postProgram, "u_glitch_intensity"),
|
|
isVpnOn ? 0.3 : 0.0,
|
|
);
|
|
|
|
gl.uniform1f(
|
|
gl.getUniformLocation(postProgram, "u_isDarkMode"),
|
|
isDarkMode ? 1.0 : 0.0,
|
|
);
|
|
|
|
// Рисуем финальный прямоугольник с эффектами
|
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
}
|
|
|
|
// --- ОБРАБОТЧИК СООБЩЕНИЙ ОТ REACT ---
|
|
self.onmessage = async (e) => {
|
|
const { type, payload } = e.data;
|
|
|
|
if (type === "INIT") {
|
|
gl = payload.canvas.getContext("webgl2", {
|
|
alpha: false,
|
|
antialias: false,
|
|
});
|
|
width = payload.logicalWidth;
|
|
height = payload.logicalHeight;
|
|
pixelScale = payload.pixelScale;
|
|
isMobile = payload.isMobile;
|
|
isLowQuality = payload.isLowQuality;
|
|
eyeScale = isMobile ? 1.2 : 1.5;
|
|
isVpnOn = payload.isVpnOn;
|
|
isDarkMode = payload.isDarkMode;
|
|
fontSize = payload.fontSize || 16;
|
|
|
|
overlayCanvas = new OffscreenCanvas(
|
|
width * pixelScale,
|
|
height * pixelScale,
|
|
);
|
|
overlayCtx = overlayCanvas.getContext("2d", { alpha: true });
|
|
overlayCtx.scale(pixelScale, pixelScale);
|
|
|
|
initWebGL();
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, postTexture);
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RGBA,
|
|
width * pixelScale,
|
|
height * pixelScale,
|
|
0,
|
|
gl.RGBA,
|
|
gl.UNSIGNED_BYTE,
|
|
null,
|
|
);
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, postFramebuffer);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
postTexture,
|
|
0,
|
|
);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
|
|
buildAtlas();
|
|
|
|
try {
|
|
const module = await import("./matrix_engine.js?v=" + version);
|
|
const init = module.default;
|
|
MatrixEngineClass = module.MatrixEngine;
|
|
|
|
// Передаем объект для инициализации (исправление warning)
|
|
wasm = await init({
|
|
module_or_path: "./matrix_engine_bg.wasm?v=" + version,
|
|
});
|
|
|
|
engine = new MatrixEngineClass(width, height, fontSize);
|
|
|
|
if (typeof engine.set_mobile === "function") engine.set_mobile(isMobile);
|
|
if (typeof engine.set_vpn_status === "function")
|
|
engine.set_vpn_status(isVpnOn);
|
|
if (payload.eyeY && typeof engine.set_eye_anchor === "function")
|
|
engine.set_eye_anchor(payload.eyeY);
|
|
|
|
// Запускаем главный цикл
|
|
requestAnimationFrame(render);
|
|
self.postMessage({ type: "READY" });
|
|
} catch (err) {
|
|
console.error("Critical failure loading WASM core in Worker:", err);
|
|
}
|
|
} else if (type === "RESIZE") {
|
|
if (!engine) return;
|
|
width = payload.logicalWidth;
|
|
height = payload.logicalHeight;
|
|
pixelScale = payload.pixelScale;
|
|
fontSize = payload.fontSize || 16;
|
|
isLowQuality = payload.isLowQuality;
|
|
|
|
gl.canvas.width = width * pixelScale;
|
|
gl.canvas.height = height * pixelScale;
|
|
|
|
overlayCanvas.width = width * pixelScale;
|
|
overlayCanvas.height = height * pixelScale;
|
|
overlayCtx.scale(pixelScale, pixelScale);
|
|
|
|
if (typeof engine.set_mobile === "function")
|
|
engine.set_mobile(payload.isMobile);
|
|
|
|
eyeScale = isMobile ? 1.2 : 1.5;
|
|
engine.resize(width, height);
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, postTexture);
|
|
gl.texImage2D(
|
|
gl.TEXTURE_2D,
|
|
0,
|
|
gl.RGBA,
|
|
width * pixelScale,
|
|
height * pixelScale,
|
|
0,
|
|
gl.RGBA,
|
|
gl.UNSIGNED_BYTE,
|
|
null,
|
|
);
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, postFramebuffer);
|
|
gl.framebufferTexture2D(
|
|
gl.FRAMEBUFFER,
|
|
gl.COLOR_ATTACHMENT0,
|
|
gl.TEXTURE_2D,
|
|
postTexture,
|
|
0,
|
|
);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
|
|
if (payload.eyeY && typeof engine.set_eye_anchor === "function")
|
|
engine.set_eye_anchor(payload.eyeY);
|
|
buildAtlas();
|
|
} else if (type === "THEME") {
|
|
// Сохраняем статус безусловно!
|
|
isVpnOn = payload.isVpnOn;
|
|
isDarkMode = payload.isDarkMode;
|
|
|
|
// А вот в движок передаем только если он уже готов
|
|
if (engine) {
|
|
if (typeof engine.set_vpn_status === "function")
|
|
engine.set_vpn_status(isVpnOn);
|
|
buildAtlas();
|
|
}
|
|
if (typeof engine.set_vpn_status === "function")
|
|
engine.set_vpn_status(isVpnOn);
|
|
buildAtlas();
|
|
} else if (type === "MOUSE_MOVE") {
|
|
if (engine) engine.update_mouse(payload.x, payload.y);
|
|
} else if (type === "DRAW_START") {
|
|
if (engine) {
|
|
engine.set_drawing(true);
|
|
engine.trigger_glitch(payload.x, payload.y);
|
|
}
|
|
} else if (type === "DRAW_END") {
|
|
if (engine) engine.set_drawing(false);
|
|
} else if (type === "SHOCKWAVE") {
|
|
if (engine) engine.trigger_shockwave(payload.x, payload.y);
|
|
} else if (type === "KINETIC_SWIPE") {
|
|
if (engine) engine.kinetic_swipe(payload.x, payload.y);
|
|
} else if (type === "TILT") {
|
|
if (engine && typeof engine.set_tilt === "function")
|
|
engine.set_tilt(payload.x, payload.y);
|
|
}
|
|
};
|