Files
netrunner-landing/matrix-engine/matrix_worker.js
T
2026-04-30 17:31:50 +07:00

578 lines
18 KiB
JavaScript

import {
vsMatrix,
vsOverlay,
vsGlitch,
fsMatrix,
fsGlitch,
postProcessSource,
} from "./shaders.js";
import { compileShader, createProgram } from "./gl-utils.js";
let engine, wasm;
let gl;
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 isPaused = false;
let rAF_ID = null;
let showEyes = true;
let fpsHistory = [];
let lastFrameTime = 0;
let scaleCheckTimer = 0;
let currentPixelScale = 1.0;
let maxPixelScale = 1.0;
const version = "3.0.3";
let vpnTimer = 0.0;
let uVpnTimerLoc;
let mouseX = 0;
let mouseY = 0;
let uMouseLoc;
let postProgram, matrixProgram, glitchProgram;
let postFramebuffer, postTexture;
let instanceBuffer, quadBuffer, overlayQuadBuffer, glitchInstanceBuffer;
let vaoMatrix, vaoOverlay, vaoGlitch;
let uResLoc, uFontLoc, uAtlasSizeLoc;
let instanceBufferCapacity = 0;
let uPostEyePosLLoc,
uPostEyePosRLoc,
uPostEyeClosednessLoc,
uPostIsVpnOnLoc,
uPostEyeActiveLoc,
uShockwaveLoc,
uPostGazeLoc;
const chars =
"0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레ро와원>_±÷×=≠≈≡≤≥";
function initWebGL() {
matrixProgram = createProgram(gl, vsMatrix, fsMatrix);
postProgram = createProgram(gl, vsOverlay, postProcessSource);
glitchProgram = createProgram(gl, vsGlitch, fsGlitch);
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,
);
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 = 10000;
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);
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;
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);
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");
uMouseLoc = gl.getUniformLocation(matrixProgram, "u_mouse");
atlasTexture = gl.createTexture();
uShockwaveLoc = gl.getUniformLocation(postProgram, "u_shockwave");
uPostEyePosLLoc = gl.getUniformLocation(postProgram, "u_eyePosL");
uPostEyePosRLoc = gl.getUniformLocation(postProgram, "u_eyePosR");
uPostEyeClosednessLoc = gl.getUniformLocation(postProgram, "u_eyeClosedness");
uPostIsVpnOnLoc = gl.getUniformLocation(postProgram, "u_isVpnOn");
uPostEyeActiveLoc = gl.getUniformLocation(postProgram, "u_eyeActive");
uPostGazeLoc = gl.getUniformLocation(postProgram, "u_gaze");
uVpnTimerLoc = gl.getUniformLocation(postProgram, "u_vpnTimer");
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;
fpsHistory.push(dt);
if (fpsHistory.length > 60) fpsHistory.shift();
scaleCheckTimer++;
if (scaleCheckTimer >= 60) {
const avgDt = fpsHistory.reduce((a, b) => a + b, 0) / fpsHistory.length;
let needsResize = false;
if (avgDt > 25.0 && currentPixelScale > 0.75) {
currentPixelScale -= 0.1;
needsResize = true;
} else if (avgDt < 15.0 && currentPixelScale < maxPixelScale) {
currentPixelScale = Math.min(maxPixelScale, currentPixelScale + 0.25);
needsResize = true;
}
if (needsResize) {
gl.canvas.width = width * currentPixelScale;
gl.canvas.height = height * currentPixelScale;
gl.bindTexture(gl.TEXTURE_2D, postTexture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.canvas.width,
gl.canvas.height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null,
);
}
scaleCheckTimer = 0;
}
}
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) {
r = isDarkMode ? 178 : 106;
g = isDarkMode ? 51 : 0;
b = isDarkMode ? 255 : 209;
} else {
r = isDarkMode ? 0 : 0;
g = isDarkMode ? 230 : 119;
b = isDarkMode ? 255 : 182;
}
atlasCtx.font = `bold ${fontSize}px monospace`;
atlasCtx.textBaseline = "top";
atlasCtx.textAlign = "center";
const halfFs = fontSize / 2;
for (let row = 0; row <= 9; row++) {
const alphaBase = isLowQuality ? 1.0 : (row + 1) / 10;
const alpha = isDarkMode ? alphaBase : alphaBase * 0.8;
atlasCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
for (let i = 0; i < chars.length; i++)
atlasCtx.fillText(chars[i], i * fontSize + halfFs, row * fontSize);
}
atlasCtx.fillStyle = "rgb(255, 255, 255)";
for (let i = 0; i < chars.length; i++)
atlasCtx.fillText(chars[i], i * fontSize + halfFs, 10 * fontSize);
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 render(currentTime) {
if (isPaused) {
rAF_ID = null;
return;
}
rAF_ID = requestAnimationFrame(render);
if (!engine || !wasm || !gl) return;
const now = currentTime || performance.now();
if (now - lastDrawTime < 16) return;
lastDrawTime = now;
updateDynamicScaling(now);
engine.tick();
const bgR = isDarkMode ? 10 / 255 : 250 / 255;
const bgG = isDarkMode ? 10 / 255 : 250 / 255;
const bgB = isDarkMode ? 12 / 255 : 252 / 255;
gl.bindFramebuffer(gl.FRAMEBUFFER, postFramebuffer);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(bgR, bgG, bgB, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
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);
gl.uniform2f(uMouseLoc, mouseX / width, mouseY / 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);
}
const glLen = engine.glitch_len();
if (glLen > 0) {
const glitchInstanceCount = glLen / 5;
gl.useProgram(glitchProgram);
gl.bindVertexArray(vaoGlitch);
let tr, tg, tb;
if (isVpnOn && isDarkMode) {
tr = 178 / 255;
tg = 51 / 255;
tb = 255 / 255;
} else if (!isVpnOn && isDarkMode) {
tr = 0;
tg = 230 / 255;
tb = 255 / 255;
} else if (isVpnOn && !isDarkMode) {
tr = 106 / 255;
tg = 0;
tb = 209 / 255;
} else {
tr = 0;
tg = 119 / 255;
tb = 182 / 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);
}
const eyeLen = engine.eye_len();
let eyeActive = 0.0,
eyeClosedness = 1.0,
posLX = 0,
posLY = 0,
posRX = 0,
posRY = 0,
normGazeX = 0,
normGazeY = 0;
if (eyeLen >= 6 && showEyes) {
const eyeData = new Float32Array(
wasm.memory.buffer,
engine.eye_ptr(),
eyeLen,
);
eyeScale = isMobile ? 1.3 : 1.8;
eyeClosedness = eyeData[5];
eyeActive = 1.0;
let baseGap = isMobile ? (width < 400 ? 55.0 : 65.0) : 75.0;
posLX = eyeData[0] - baseGap * eyeScale;
posLY = eyeData[1];
posRX = eyeData[0] + baseGap * eyeScale;
posRY = eyeData[1];
normGazeX = (eyeData[2] * eyeScale) / (15.0 * eyeScale);
normGazeY = (eyeData[3] * eyeScale) / (-10.0 * eyeScale);
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
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"),
gl.canvas.width,
gl.canvas.height,
);
gl.uniform1f(gl.getUniformLocation(postProgram, "u_distortion"), 18.0);
gl.uniform1f(
gl.getUniformLocation(postProgram, "u_isDarkMode"),
isDarkMode ? 1.0 : 0.0,
);
gl.uniform1f(
gl.getUniformLocation(postProgram, "u_isLowQuality"),
isLowQuality ? 1.0 : 0.0,
);
gl.uniform1f(uPostEyeActiveLoc, eyeActive);
if (eyeActive > 0.5) {
gl.uniform2f(
uPostEyePosLLoc,
posLX * currentPixelScale,
posLY * currentPixelScale,
);
gl.uniform2f(
uPostEyePosRLoc,
posRX * currentPixelScale,
posRY * currentPixelScale,
);
gl.uniform1f(uPostEyeClosednessLoc, eyeClosedness);
gl.uniform1f(uPostIsVpnOnLoc, isVpnOn ? 1.0 : 0.0);
gl.uniform2f(uPostGazeLoc, normGazeX, normGazeY);
}
if (vpnTimer > 0.0) vpnTimer = Math.max(0.0, vpnTimer - 0.015);
gl.uniform1f(uVpnTimerLoc, vpnTimer);
let sw_x = -1.0,
sw_y = -1.0,
sw_r = 0.0;
if (engine.has_active_shockwave && engine.has_active_shockwave()) {
sw_x = engine.get_shockwave_x() / width;
sw_y = engine.get_shockwave_y() / height;
sw_r = engine.get_shockwave_radius() / width;
}
gl.uniform3f(uShockwaveLoc, sw_x, 1.0 - sw_y, sw_r);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
self.onmessage = async (e) => {
const { type, payload } = e.data;
if (type === "INIT") {
gl = payload.canvas.getContext("webgl2", {
alpha: false,
antialias: false,
powerPreference: "high-performance",
});
width = payload.logicalWidth;
height = payload.logicalHeight;
pixelScale = payload.pixelScale;
maxPixelScale = pixelScale;
currentPixelScale = pixelScale;
isMobile = payload.isMobile;
isLowQuality = payload.isLowQuality;
eyeScale = isMobile ? 1.5 : 1.8;
isVpnOn = payload.isVpnOn;
isDarkMode = payload.isDarkMode;
fontSize = payload.fontSize || 16;
showEyes = payload.showEyes !== false;
// ВАЖНО: Устанавливаем физический размер канваса ПЕРЕД инициализацией текстур
gl.canvas.width = width * currentPixelScale;
gl.canvas.height = height * currentPixelScale;
initWebGL();
// Инициализируем текстуру пост-процессинга ПРАВИЛЬНЫМ размером сразу
gl.bindTexture(gl.TEXTURE_2D, postTexture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.canvas.width,
gl.canvas.height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null,
);
gl.bindFramebuffer(gl.FRAMEBUFFER, postFramebuffer);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
postTexture,
0,
);
buildAtlas();
try {
const cleanPath = payload.assetsPath.endsWith("/")
? payload.assetsPath
: `${payload.assetsPath}/`;
const module = await import(`${cleanPath}matrix_engine.js?v=${version}`);
wasm = await module.default({
module_or_path: `${cleanPath}matrix_engine_bg.wasm?v=${version}`,
});
engine = new module.MatrixEngine(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);
for (let i = 0; i < 300; i++) engine.tick();
if (!isPaused) rAF_ID = requestAnimationFrame(render);
self.postMessage({ type: "READY" });
} catch (err) {
console.error("Critical failure loading WASM core:", err);
}
} else if (type === "PAUSE") {
isPaused = true;
if (rAF_ID !== null) {
cancelAnimationFrame(rAF_ID);
rAF_ID = null;
}
} else if (type === "RESUME") {
if (isPaused) {
isPaused = false;
const now = performance.now();
lastDrawTime = now;
lastFrameTime = now;
if (rAF_ID === null && engine) rAF_ID = requestAnimationFrame(render);
}
} else if (type === "RESIZE") {
if (!engine) return;
width = payload.logicalWidth;
height = payload.logicalHeight;
currentPixelScale = payload.pixelScale;
maxPixelScale = currentPixelScale;
gl.canvas.width = width * currentPixelScale;
gl.canvas.height = height * currentPixelScale;
gl.bindTexture(gl.TEXTURE_2D, postTexture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.canvas.width,
gl.canvas.height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null,
);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
engine.resize(width, height);
buildAtlas();
} else if (type === "THEME") {
isVpnOn = payload.isVpnOn;
isDarkMode = payload.isDarkMode;
vpnTimer = 1.0;
if (engine) {
if (typeof engine.set_vpn_status === "function")
engine.set_vpn_status(isVpnOn);
buildAtlas();
}
} else if (type === "MOUSE_MOVE") {
mouseX = payload.x;
mouseY = payload.y;
if (engine) engine.update_mouse(payload.x, payload.y);
} else if (type === "DRAW_START") {
if (engine) {
engine.set_drawing(true);
if (Math.random() > 0.5) engine.trigger_glitch(payload.x, payload.y);
else engine.trigger_shockwave(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 === "TILT") {
if (engine && typeof engine.set_tilt === "function")
engine.set_tilt(payload.x, payload.y);
}
};