Files
netrunner-landing/matrix-engine/matrix_worker.js
T
2026-04-26 19:59:47 +07:00

1185 lines
36 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 interactionTimer = 0;
let fpsHistory = [];
let lastFrameTime = 0;
let scaleCheckTimer = 0;
let currentPixelScale = 1.0;
let maxPixelScale = 1.0;
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 vsGlitch = `#version 300 es
in vec2 a_quad; // 0..1
in vec4 a_glitchData; // x, y, w, h
in float a_colorType; // 0..4
uniform vec2 u_resolution;
out float v_colorType;
void main() {
// a_glitchData: x = pos.x, y = pos.y, z = width, w = height
vec2 pixelPos = vec2(a_glitchData.x, a_glitchData.y) + a_quad * vec2(a_glitchData.z, a_glitchData.w);
// Переводим пиксели (0..ширина) в пространство 0..1
vec2 zeroToOne = pixelPos / u_resolution;
// Переводим 0..1 в пространство -1..1 (Clip Space)
vec2 clipSpace = (zeroToOne * 2.0) - 1.0;
// Переворачиваем Y, так как в WebGL Y идет вверх
gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);
v_colorType = a_colorType;
}
`.trim();
const fsGlitch = `#version 300 es
precision mediump float;
in float v_colorType;
uniform vec3 u_themeMain;
uniform vec3 u_themeBg;
out vec4 outColor;
void main() {
int type = int(v_colorType + 0.1); // Добавили epsilon для точности
if (type == 0) {
outColor = vec4(u_themeBg, 1.0);
} else if (type == 1) {
outColor = vec4(u_themeMain, 0.9);
} else if (type == 2) {
outColor = vec4(vec3(1.0) - u_themeMain, 0.7);
} else if (type == 3) {
outColor = vec4(1.0, 1.0, 1.0, 0.95);
} else if (type == 4) {
outColor = vec4(1.0, 0.08, 0.31, 0.95); // Тот самый красный
} else {
discard; // Если тип странный — просто не рисуем
}
}
`.trim();
const vsEye = `#version 300 es
in vec2 a_quadCentered; // -0.5 to 0.5
uniform vec2 u_resolution;
uniform vec2 u_eyePos; // Центр глаза
uniform float u_eyeSize;
out vec2 v_uv;
void main() {
vec2 pos = u_eyePos + a_quadCentered * u_eyeSize;
vec2 zeroToOne = pos / u_resolution;
vec2 clipSpace = (zeroToOne * 2.0) - 1.0;
gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);
v_uv = a_quadCentered + 0.5; // Переводим -0.5..0.5 в 0..1 для текстуры
}
`.trim();
const fsEye = `#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);
// Цвет фона: жестко привязываем к теме
vec3 current_bg = u_isDarkMode > 0.5 ? vec3(10.0/255.0, 10.0/255.0, 12.0/255.0) : vec3(250.0/255.0, 250.0/255.0, 252.0/255.0);
// Если вышли за границы "пузатого" монитора
if (crt_uv.x < 0.0 || crt_uv.x > 1.0 || crt_uv.y < 0.0 || crt_uv.y > 1.0) {
outColor = vec4(current_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();
let matrixProgram, overlayProgram;
let instanceBuffer, quadBuffer, overlayQuadBuffer;
let vaoMatrix, vaoOverlay;
let uResLoc, uFontLoc, uAtlasSizeLoc;
let overlayTexture;
let instanceBufferCapacity = 0;
// ДОБАВЛЕННЫЕ ПЕРЕМЕННЫЕ ДЛЯ ГЛИТЧЕЙ И ГЛАЗ
let glitchProgram, vaoGlitch, glitchInstanceBuffer;
let eyeProgram, vaoEye, quadBufferCentered;
let eyeCanvas, eyeCtx, eyeTexture;
// --- ИНИЦИАЛИЗАЦИЯ 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);
postProgram = createProgram(vsOverlay, postProcessSource);
glitchProgram = createProgram(vsGlitch, fsGlitch);
eyeProgram = createProgram(vsEye, fsEye);
// --- БАЗОВЫЕ КВАДРАТЫ ---
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,
);
quadBufferCentered = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBufferCentered);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5,
]),
gl.STATIC_DRAW,
);
// --- VAO: МАТРИЦА ---
vaoMatrix = gl.createVertexArray();
gl.bindVertexArray(vaoMatrix);
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
const aQuadLoc = gl.getAttribLocation(matrixProgram, "a_quad");
gl.enableVertexAttribArray(aQuadLoc);
gl.vertexAttribPointer(aQuadLoc, 2, gl.FLOAT, false, 0, 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);
// --- VAO: ГЛИТЧИ ---
vaoGlitch = gl.createVertexArray();
gl.bindVertexArray(vaoGlitch);
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
const aQuadGlitch = gl.getAttribLocation(glitchProgram, "a_quad");
gl.enableVertexAttribArray(aQuadGlitch);
gl.vertexAttribPointer(aQuadGlitch, 2, gl.FLOAT, false, 0, 0);
glitchInstanceBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, glitchInstanceBuffer);
const strideG = 5 * 4; // БЫЛО 6, СТАЛО 5 (согласно Rust структуре)
const aDataLoc = gl.getAttribLocation(glitchProgram, "a_glitchData");
gl.enableVertexAttribArray(aDataLoc);
gl.vertexAttribPointer(aDataLoc, 4, gl.FLOAT, false, strideG, 0);
gl.vertexAttribDivisor(aDataLoc, 1);
const aColorLoc = gl.getAttribLocation(glitchProgram, "a_colorType");
gl.enableVertexAttribArray(aColorLoc);
gl.vertexAttribPointer(aColorLoc, 1, gl.FLOAT, false, strideG, 4 * 4);
gl.vertexAttribDivisor(aColorLoc, 1);
// --- VAO: ГЛАЗА (Sprite) ---
vaoEye = gl.createVertexArray();
gl.bindVertexArray(vaoEye);
gl.bindBuffer(gl.ARRAY_BUFFER, quadBufferCentered);
const aQuadEye = gl.getAttribLocation(eyeProgram, "a_quadCentered");
gl.enableVertexAttribArray(aQuadEye);
gl.vertexAttribPointer(aQuadEye, 2, gl.FLOAT, false, 0, 0);
// --- VAO: POST PROCESS (Overlay Quad) ---
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(postProgram, "a_position");
gl.enableVertexAttribArray(aPosLocOver);
gl.vertexAttribPointer(aPosLocOver, 2, gl.FLOAT, false, 0, 0);
gl.bindVertexArray(null);
// --- ТЕКСТУРЫ И БУФЕРЫ ---
uResLoc = gl.getUniformLocation(matrixProgram, "u_resolution");
uFontLoc = gl.getUniformLocation(matrixProgram, "u_fontSize");
uAtlasSizeLoc = gl.getUniformLocation(matrixProgram, "u_atlasSize");
atlasTexture = gl.createTexture();
// Инициализация маленького канваса для глаз (300x300 пикселей)
eyeCanvas = new OffscreenCanvas(300, 300);
eyeCtx = eyeCanvas.getContext("2d", { alpha: true });
eyeTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, eyeTexture);
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);
postTexture = gl.createTexture();
postFramebuffer = gl.createFramebuffer();
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);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
}
function updateDynamicScaling(now) {
if (lastFrameTime === 0) lastFrameTime = now;
const dt = now - lastFrameTime;
lastFrameTime = now;
// Собираем историю последних 60 кадров
fpsHistory.push(dt);
if (fpsHistory.length > 60) fpsHistory.shift();
scaleCheckTimer++;
// Проверяем производительность каждую секунду (60 кадров)
if (scaleCheckTimer >= 60) {
const avgDt = fpsHistory.reduce((a, b) => a + b, 0) / fpsHistory.length;
let needsResize = false;
// Если FPS падает ниже ~45-50 (кадр дольше 22 мс)
if (avgDt > 22.0 && currentPixelScale > 0.5) {
currentPixelScale = Math.max(0.5, currentPixelScale - 0.25);
needsResize = true;
}
// Если летит стабильно 60+ FPS (кадр меньше 15 мс) и мы не на максимуме
else if (avgDt < 15.0 && currentPixelScale < maxPixelScale) {
currentPixelScale = Math.min(maxPixelScale, currentPixelScale + 0.25);
needsResize = true;
}
if (needsResize) {
console.log(`[AutoScaler] Adjusting pixelScale to: ${currentPixelScale}`);
// Применяем новый скейл к буферам WebGL
gl.canvas.width = width * currentPixelScale;
gl.canvas.height = height * currentPixelScale;
gl.bindTexture(gl.TEXTURE_2D, postTexture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
width * currentPixelScale,
height * currentPixelScale,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null,
);
// Если используешь overlayCanvas для глаз (см. ниже), его тоже ресайзим
}
scaleCheckTimer = 0;
}
}
function renderSingleEyeToTexture(gazeX, gazeY, dir, isClosed) {
// Очищаем маленький канвас
eyeCtx.clearRect(0, 0, 300, 300);
// Передаем cx = 150, cy = 150 (центр 300x300)
drawEye(eyeCtx, 150, 150, gazeX, gazeY, dir, isClosed);
// Загружаем результат в текстуру
gl.bindTexture(gl.TEXTURE_2D, eyeTexture);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
// ИСПРАВЛЕНИЕ: Ставим false, чтобы текстура не переворачивалась дважды
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
eyeCanvas,
);
}
function buildAtlas() {
atlasCanvas = new OffscreenCanvas(chars.length * fontSize, fontSize * 11);
atlasCtx = atlasCanvas.getContext("2d", { alpha: true });
// 1. Полная очистка
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;
// 2. Рисуем ТОЛЬКО текст. Никаких fillRect под ним!
for (let row = 0; row <= 9; row++) {
const alpha = isLowQuality ? 1.0 : (row + 1) / 10;
const yOffset = row * fontSize;
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;
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);
}
}
// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ОТРИСОВКИ ---
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;
interactionTimer = 120;
// ОПТИМИЗАЦИЯ ДЛЯ СЛАБЫХ УСТРОЙСТВ
// На старом ноуте/Xiaomi обновляем тяжелую текстуру глаз в 2 раза реже
if (isLowQuality && frameCounter % 2 !== 0) {
// Пропускаем очистку и отрисовку в этом кадре,
// WebGL просто выведет старую текстуру из кэша
return true;
}
overlayCtx.globalAlpha = 1.0;
const eyeData = new Float32Array(
wasm.memory.buffer,
engine.eye_ptr(),
eyeLen,
);
// Исправленный gap: на мобилках глаза ближе друг к другу (60 вместо 85)
const gap = (isMobile ? 75.0 : 80.0) * 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],
);
} else if (interactionTimer > 0) {
interactionTimer--;
}
return hasContent;
}
function render(currentTime) {
requestAnimationFrame(render);
if (!engine || !wasm || !gl) return;
const now = currentTime || performance.now();
if (now - lastDrawTime < 16) return;
lastDrawTime = now;
// 1. Авто-Скейлер
updateDynamicScaling(now);
// 2. Физика
engine.tick();
// 3. Подготовка буферов
const targetBuffer = isLowQuality ? null : postFramebuffer;
gl.bindFramebuffer(gl.FRAMEBUFFER, targetBuffer);
gl.viewport(0, 0, width * currentPixelScale, height * currentPixelScale);
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: МАТРИЦА (Zero-Copy Instancing)
// ==========================================
const renderLen = engine.render_len();
if (renderLen > 0) {
const instanceCount = renderLen / 5;
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);
}
// ==========================================
// ЭТАП 2: ГЛИТЧИ (Pure WebGL Instancing)
// ==========================================
const glLen = engine.glitch_len();
if (glLen > 0) {
const glitchInstanceCount = glLen / 5;
gl.useProgram(glitchProgram);
gl.bindVertexArray(vaoGlitch);
// Определяем цвета темы
let tr = 2 / 255,
tg = 132 / 255,
tb = 199 / 255; // Blue
if (isVpnOn && isDarkMode) {
tr = 139 / 255;
tg = 61 / 255;
tb = 255 / 255;
} else if (!isVpnOn && isDarkMode) {
tr = 0;
tg = 229 / 255;
tb = 242 / 255;
} else if (isVpnOn && !isDarkMode) {
tr = 109 / 255;
tg = 40 / 255;
tb = 217 / 255;
}
gl.uniform3f(
gl.getUniformLocation(glitchProgram, "u_themeMain"),
tr,
tg,
tb,
);
gl.uniform3f(
gl.getUniformLocation(glitchProgram, "u_themeBg"),
bgR,
bgG,
bgB,
);
gl.uniform2f(
gl.getUniformLocation(glitchProgram, "u_resolution"),
width,
height,
);
const glDataView = new Float32Array(
wasm.memory.buffer,
engine.glitch_ptr(),
glLen,
);
gl.bindBuffer(gl.ARRAY_BUFFER, glitchInstanceBuffer);
gl.bufferData(gl.ARRAY_BUFFER, glDataView, gl.DYNAMIC_DRAW);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, glitchInstanceCount);
}
// ==========================================
// ЭТАП 3: ГЛАЗА (Sprite Batching)
// ==========================================
const eyeLen = engine.eye_len();
if (eyeLen >= 6) {
interactionTimer = 120;
const eyeData = new Float32Array(
wasm.memory.buffer,
engine.eye_ptr(),
eyeLen,
);
const eyeBaseX = eyeData[0];
const eyeBaseY = eyeData[1];
// Обновляем текстуру глаз ТОЛЬКО если мы не пропускаем кадр из-за lowQuality
if (!isLowQuality || frameCounter % 2 === 0) {
renderSingleEyeToTexture(eyeData[2], eyeData[3], eyeData[4], eyeData[5]);
}
gl.useProgram(eyeProgram);
gl.bindVertexArray(vaoEye);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, eyeTexture);
gl.uniform1i(gl.getUniformLocation(eyeProgram, "u_texture"), 0);
gl.uniform2f(
gl.getUniformLocation(eyeProgram, "u_resolution"),
width,
height,
);
gl.uniform1f(gl.getUniformLocation(eyeProgram, "u_eyeSize"), 300.0);
const gap = (isMobile ? 75.0 : 80.0) * eyeScale;
// Рисуем ЛЕВЫЙ глаз
gl.uniform2f(
gl.getUniformLocation(eyeProgram, "u_eyePos"),
eyeBaseX - gap,
eyeBaseY,
);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Рисуем ПРАВЫЙ глаз
gl.uniform2f(
gl.getUniformLocation(eyeProgram, "u_eyePos"),
eyeBaseX + gap,
eyeBaseY,
);
gl.drawArrays(gl.TRIANGLES, 0, 6);
} else if (interactionTimer > 0) {
interactionTimer--;
}
frameCounter++;
// ==========================================
// ЭТАП 4: ПОСТ-ПРОЦЕССИНГ (Для мощных ПК)
// ==========================================
if (isLowQuality) return;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.useProgram(postProgram);
gl.bindVertexArray(vaoOverlay);
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 * currentPixelScale,
height * currentPixelScale,
);
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();
}
} 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);
}
};