Files
netrunner-landing/matrix-engine/matrix_worker.js
T
2026-04-26 12:32:57 +07:00

455 lines
12 KiB
JavaScript

/**
* MATRIX_WORKER.JS
* Реализация с динамическим версионированием для обхода кэша Cloudflare
*/
let engine;
let wasm;
let ctx;
let atlasCanvas, atlasCtx;
let width, height;
let fontSize = 16;
let isMobile = false,
isVpnOn = false,
isDarkMode = true;
let lastDrawTime = 0;
// Ссылка на класс, которую мы получим после импорта модуля
let MatrixEngineClass;
// 1. Извлекаем версию из параметров запуска самого воркера
const workerUrlParams = new URLSearchParams(self.location.search);
const version = workerUrlParams.get("v") || Date.now().toString();
const chars =
"0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레로와원>_±÷×=≠≈≡≤≥";
function buildAtlas() {
// Увеличиваем атлас до 11 строк
atlasCanvas = new OffscreenCanvas(chars.length * fontSize, fontSize * 11);
// ВАЖНО: alpha: true, чтобы фон мог быть полупрозрачным!
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;
}
const bgR = isDarkMode ? 10 : 250;
const bgG = isDarkMode ? 10 : 250;
const bgB = isDarkMode ? 12 : 252;
atlasCtx.font = `bold ${fontSize}px monospace`;
atlasCtx.textBaseline = "top";
atlasCtx.textAlign = "center";
const halfFs = fontSize / 2;
// Строки 0..9: Хвост (запекаем альфу прямо в картинку)
for (let row = 0; row <= 9; row++) {
const alpha = (row + 1) / 10;
const yOffset = row * fontSize;
// Рисуем полупрозрачный фон
atlasCtx.fillStyle = `rgba(${bgR}, ${bgG}, ${bgB}, ${alpha})`;
atlasCtx.fillRect(0, yOffset, atlasCanvas.width, fontSize);
// Рисуем полупрозрачный текст
atlasCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
for (let i = 0; i < chars.length; i++) {
atlasCtx.fillText(chars[i], i * fontSize + halfFs, yOffset);
}
}
// Строка 10: Белая "Голова" (Альфа 1.0)
const headY = 10 * fontSize;
atlasCtx.fillStyle = `rgb(${bgR}, ${bgG}, ${bgB})`;
atlasCtx.fillRect(0, headY, atlasCanvas.width, fontSize);
atlasCtx.fillStyle = "rgb(255, 255, 255)";
for (let i = 0; i < chars.length; i++) {
atlasCtx.fillText(chars[i], i * fontSize + halfFs, headY);
}
}
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)";
}
let pixelScale = 1.0;
self.onmessage = async (e) => {
const { type, payload } = e.data;
if (type === "INIT") {
ctx = payload.canvas.getContext("2d", { alpha: false });
width = payload.logicalWidth;
height = payload.logicalHeight;
pixelScale = payload.pixelScale;
isMobile = payload.isMobile;
isVpnOn = payload.isVpnOn;
isDarkMode = payload.isDarkMode;
fontSize = payload.fontSize || 16;
try {
const module = await import("./matrix_engine.js?v=" + version);
const init = module.default;
MatrixEngineClass = module.MatrixEngine;
wasm = await init("./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);
// АППАРАТНОЕ МАСШТАБИРОВАНИЕ
ctx.scale(pixelScale, pixelScale);
buildAtlas();
requestAnimationFrame(render);
} 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;
// Переустанавливаем физический размер и заново применяем Scale
ctx.canvas.width = width * pixelScale;
ctx.canvas.height = height * pixelScale;
ctx.scale(pixelScale, pixelScale);
if (typeof engine.set_mobile === "function")
engine.set_mobile(payload.isMobile);
engine.resize(width, height);
if (payload.eyeY && typeof engine.set_eye_anchor === "function")
engine.set_eye_anchor(payload.eyeY);
buildAtlas();
} else if (type === "THEME") {
if (!engine) return;
isVpnOn = payload.isVpnOn;
isDarkMode = payload.isDarkMode;
if (typeof engine.set_vpn_status === "function")
engine.set_vpn_status(isVpnOn);
buildAtlas();
if (ctx) {
ctx.globalAlpha = 1.0;
ctx.fillStyle = isDarkMode ? "rgb(10, 10, 12)" : "rgb(250, 250, 252)";
ctx.fillRect(0, 0, width, height); // fillRect автоматически растянется из-за ctx.scale!
}
} 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);
}
};
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;
const eyeWidth = 120 * (1 - isClosed) + 140 * isClosed;
const eyeRadius = 36 * (1 - isClosed) + 8 * isClosed;
const laserWidth = 140 * isClosed;
const blurMultiplier = isMobile ? 0 : 1.0;
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 = 4 * (1 - isClosed) + 3 * isClosed;
ctx.shadowColor = color;
ctx.shadowBlur = (15 * (1 - isClosed) + 8 * 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, cy + gazeY);
const t = performance.now() * 0.003; // Ускорено вращение колец
ctx.save();
ctx.rotate(t);
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.globalAlpha = 0.7 * alphaMult;
ctx.setLineDash([8, 6, 2, 6]);
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(0, 0, 16, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
ctx.strokeStyle = isDarkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
ctx.lineWidth = 1;
ctx.globalAlpha = alphaMult;
ctx.beginPath();
ctx.arc(0, 0, 10, 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);
ctx.lineTo(5, 0);
ctx.lineTo(0, 6);
ctx.lineTo(-5, 0);
ctx.fill();
ctx.fillStyle = color;
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(0, 0, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.shadowBlur = 5 * blurMultiplier;
ctx.globalAlpha = 0.4 * alphaMult;
ctx.beginPath();
ctx.moveTo(-22, 0);
ctx.lineTo(-12, 0);
ctx.moveTo(22, 0);
ctx.lineTo(12, 0);
ctx.moveTo(0, -22);
ctx.lineTo(0, -12);
ctx.moveTo(0, 22);
ctx.lineTo(0, 12);
ctx.stroke();
ctx.fillStyle = color;
ctx.font = "10px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.shadowBlur = 8 * blurMultiplier;
ctx.globalAlpha = (dir === 1 ? 1.0 : 0.15) * alphaMult;
ctx.fillText("▴", 0, -26);
ctx.globalAlpha = (dir === 2 ? 1.0 : 0.15) * alphaMult;
ctx.fillText("▾", 0, 26);
ctx.globalAlpha = (dir === 3 ? 1.0 : 0.15) * alphaMult;
ctx.fillText("◂", -26, 1);
ctx.globalAlpha = (dir === 4 ? 1.0 : 0.15) * alphaMult;
ctx.fillText("▸", 26, 1);
ctx.restore();
}
ctx.restore();
}
// === РАЗДЕЛЕНИЕ ЛОГИКИ ОТРИСОВКИ ===
function drawMatrix() {
// Очистка экрана с небольшим шлейфом
ctx.globalAlpha = 1.0;
ctx.fillStyle = isDarkMode
? "rgba(10, 10, 12, 0.25)"
: "rgba(250, 250, 252, 0.25)";
ctx.fillRect(0, 0, width, height);
const renderLen = engine.render_len();
if (renderLen > 0) {
const data = new Float32Array(
wasm.memory.buffer,
engine.render_ptr(),
renderLen,
);
for (let i = 0; i < renderLen; i += 5) {
const x = data[i];
const y = data[i + 1];
const charIdx = data[i + 2];
const atlasRow = data[i + 3];
const scale = data[i + 4];
const scaledSize = fontSize * scale;
ctx.drawImage(
atlasCanvas,
charIdx * fontSize,
atlasRow * fontSize,
fontSize,
fontSize,
x,
y,
scaledSize,
scaledSize,
);
}
}
}
function drawEffects() {
// Рендер Глитч-блоков
const glLen = engine.glitch_len();
if (glLen > 0) {
ctx.globalAlpha = 1.0;
const glData = new Float32Array(
wasm.memory.buffer,
engine.glitch_ptr(),
glLen,
);
for (let i = 0; i < glLen; i += 5) {
ctx.fillStyle = getGlitchColor(glData[i + 4]);
ctx.fillRect(glData[i], glData[i + 1], glData[i + 2], glData[i + 3]);
}
}
// Рендер Искр
const gsLen = engine.glass_len();
if (gsLen > 0) {
const gsData = new Float32Array(
wasm.memory.buffer,
engine.glass_ptr(),
gsLen,
);
for (let i = 0; i < gsLen; i += 4) {
ctx.globalAlpha = gsData[i + 3];
ctx.drawImage(
atlasCanvas,
gsData[i + 2] * fontSize,
0,
fontSize,
fontSize,
gsData[i],
gsData[i + 1],
fontSize,
fontSize,
);
}
}
}
function drawEyesBuffers() {
const eyeLen = engine.eye_len();
if (eyeLen >= 6) {
const eyeData = new Float32Array(
wasm.memory.buffer,
engine.eye_ptr(),
eyeLen,
);
const cx = eyeData[0];
const cy = eyeData[1];
const gazeX = eyeData[2];
const gazeY = eyeData[3];
const dir = eyeData[4];
const isClosed = eyeData[5];
const gap = isMobile ? 85 : 85;
ctx.globalAlpha = 1.0;
drawEye(ctx, cx - gap, cy, gazeX, gazeY, dir, isClosed);
drawEye(ctx, cx + gap, cy, gazeX, gazeY, dir, isClosed);
}
}
// === ГЛАВНЫЙ ЦИКЛ ===
function render(currentTime) {
requestAnimationFrame(render);
if (!engine || !wasm) return;
const now = currentTime || performance.now();
// Адаптивный FPS
const fpsLimit = 16;
if (now - lastDrawTime < fpsLimit) return;
lastDrawTime = now;
// 1. Считаем физику (только 1 раз за кадр!)
engine.tick();
// 2. Рисуем слои
drawMatrix();
drawEffects();
drawEyesBuffers();
}