optimization and stylizing

This commit is contained in:
2026-04-26 18:49:50 +07:00
parent f22b04df33
commit 2664f00741
3 changed files with 264 additions and 63 deletions
+29 -13
View File
@@ -59,7 +59,6 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const container = containerRef.current; const container = containerRef.current;
if (!container || hasTransferred.current) return; if (!container || hasTransferred.current) return;
// Создаем canvas вручную, чтобы обойти баг React DOM reuse
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.className = canvas.className =
"absolute inset-0 w-full h-full block transition-opacity duration-1000 opacity-0 z-0"; "absolute inset-0 w-full h-full block transition-opacity duration-1000 opacity-0 z-0";
@@ -67,7 +66,17 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
try { try {
const isMobile = checkIsMobile(); const isMobile = checkIsMobile();
const pixelScale = Math.min(window.devicePixelRatio || 1, 2.0);
// --- ДЕТЕКТОР СЛАБОГО ЖЕЛЕЗА И ДАУНСЭМПЛИНГ ---
const cores = navigator.hardwareConcurrency || 4;
const memory = (navigator as any).deviceMemory || 8;
const isWeakHardware = cores <= 4 || memory <= 4;
const isLowQuality = isMobile || isWeakHardware;
// На слабом ПК и телефонах рендерим 75% от разрешения экрана
const pixelScale = isLowQuality ? 0.75 : 1.0;
// ----------------------------------------------
const logicalWidth = container.clientWidth; const logicalWidth = container.clientWidth;
const logicalHeight = container.clientHeight; const logicalHeight = container.clientHeight;
@@ -82,7 +91,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const offscreen = canvas.transferControlToOffscreen(); const offscreen = canvas.transferControlToOffscreen();
hasTransferred.current = true; hasTransferred.current = true;
const eyeScale = isMobile ? 0.6 : 0.8; const eyeScale = isMobile ? 0.8 : 1.5; // Оставили твои размеры
worker.postMessage( worker.postMessage(
{ {
@@ -98,24 +107,27 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
isVpnOn: isSecure, isVpnOn: isSecure,
isDarkMode, isDarkMode,
fontSize: isMobile ? 12 : 16, fontSize: isMobile ? 12 : 16,
isLowQuality, // Передаем флаг в воркер
}, },
}, },
[offscreen], [offscreen],
); );
// Плавное появление worker.onmessage = (e) => {
setTimeout(() => { if (e.data.type === "READY") {
canvas.classList.remove("opacity-0"); canvas.classList.remove("opacity-0");
canvas.classList.add("opacity-100"); canvas.classList.add("opacity-100");
}, 150); }
};
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) { for (let entry of entries) {
const { width, height } = entry.contentRect; const { width, height } = entry.contentRect;
if (width === 0 || height === 0) continue; if (width === 0 || height === 0) continue;
const pScale = Math.min(window.devicePixelRatio || 1, 2.0);
const isMob = width <= 768; const isMob = width <= 768;
const lowQual = isMob || isWeakHardware;
const pScale = lowQual ? 0.75 : 1.0;
workerRef.current?.postMessage({ workerRef.current?.postMessage({
type: "RESIZE", type: "RESIZE",
@@ -124,9 +136,10 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
logicalHeight: height, logicalHeight: height,
pixelScale: pScale, pixelScale: pScale,
eyeY: getEyeY(), eyeY: getEyeY(),
eyeScale: isMob ? 0.6 : 0.8, eyeScale: isMob ? 0.8 : 1.5,
isMobile: isMob, isMobile: isMob,
fontSize: isMob ? 12 : 16, fontSize: isMob ? 12 : 16,
isLowQuality: lowQual,
}, },
}); });
} }
@@ -150,15 +163,18 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
} }
}, [mounted, checkIsMobile, getEyeY]); }, [mounted, checkIsMobile, getEyeY]);
// === 2. РЕАКЦИЯ НА ТЕМУ === // === 2. РЕАКЦИЯ НА ТЕМУ И VPN (ВОЗВРАЩАЕМ!) ===
useEffect(() => { useEffect(() => {
if (workerRef.current && mounted) { if (workerRef.current && mounted) {
workerRef.current.postMessage({ workerRef.current.postMessage({
type: "THEME", type: "THEME",
payload: { isVpnOn: isSecure, isDarkMode }, payload: {
isVpnOn: isSecure,
isDarkMode: isDarkMode,
},
}); });
} }
}, [isSecure, isDarkMode, mounted]); }, [isSecure, isDarkMode, mounted]); // Теперь React сообщит воркеру об изменениях
// === 3. ИНТЕРАКТИВНОСТЬ (МЫШЬ, КЛИКИ, ГИРОСКОП) === // === 3. ИНТЕРАКТИВНОСТЬ (МЫШЬ, КЛИКИ, ГИРОСКОП) ===
useEffect(() => { useEffect(() => {
+229 -42
View File
@@ -15,7 +15,12 @@ let isMobile = false,
isDarkMode = true; isDarkMode = true;
let lastDrawTime = 0; let lastDrawTime = 0;
let pixelScale = 1.0; let pixelScale = 1.0;
let eyeScale = 0.8; let isLowQuality = false;
let frameCounter = 0;
let eyeScale = 1.5;
let postProgram;
let postFramebuffer, postTexture;
let MatrixEngineClass; let MatrixEngineClass;
const workerUrlParams = new URLSearchParams(self.location.search); const workerUrlParams = new URLSearchParams(self.location.search);
@@ -24,10 +29,9 @@ const version = workerUrlParams.get("v") || Date.now().toString();
const chars = const chars =
"0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레로와원>_±÷×=≠≈≡≤≥"; "0101アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ마미무메모야유요라릴레로와원>_±÷×=≠≈≡≤≥";
// --- WEBGL SHADERS ---
// --- WEBGL SHADERS --- // --- WEBGL SHADERS ---
const vsMatrix = `#version 300 es const vsMatrix = `#version 300 es
precision mediump float; // ИСПРАВЛЕНИЕ: уравняли точность precision mediump float;
in vec2 a_quad; in vec2 a_quad;
in vec2 a_position; in vec2 a_position;
@@ -44,17 +48,14 @@ out float v_atlasRow;
void main() { void main() {
float size = u_fontSize * a_scale; float size = u_fontSize * a_scale;
vec2 pos = a_position + a_quad * size; vec2 pos = a_position + a_quad * size;
vec2 zeroToOne = pos / u_resolution; vec2 zeroToOne = pos / u_resolution;
vec2 zeroToTwo = zeroToOne * 2.0; vec2 zeroToTwo = zeroToOne * 2.0;
vec2 clipSpace = zeroToTwo - 1.0; vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 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_uv = vec2(a_charIdx * u_fontSize + a_quad.x * u_fontSize, a_quad.y * u_fontSize);
v_atlasRow = a_atlasRow; v_atlasRow = a_atlasRow;
} }
`; `.trim(); // <--- ВОТ ТУТ МАГИЯ
const fsMatrix = `#version 300 es const fsMatrix = `#version 300 es
precision mediump float; precision mediump float;
@@ -71,24 +72,22 @@ out vec4 outColor;
void main() { void main() {
float yOffset = v_atlasRow * u_fontSize; float yOffset = v_atlasRow * u_fontSize;
vec2 finalUV = vec2(v_uv.x, v_uv.y + yOffset) / u_atlasSize; vec2 finalUV = vec2(v_uv.x, v_uv.y + yOffset) / u_atlasSize;
vec4 texColor = texture(u_atlas, finalUV); vec4 texColor = texture(u_atlas, finalUV);
if (texColor.a < 0.001) discard; if (texColor.a < 0.001) discard;
outColor = texColor; outColor = texColor;
} }
`; `.trim();
const vsOverlay = `#version 300 es const vsOverlay = `#version 300 es
precision mediump float; // ИСПРАВЛЕНИЕ: уравняли точность precision mediump float;
in vec2 a_position; in vec2 a_position;
out vec2 v_uv; out vec2 v_uv;
void main() { void main() {
v_uv = a_position * 0.5 + 0.5; v_uv = a_position * 0.5 + 0.5;
v_uv.y = 1.0 - v_uv.y;
gl_Position = vec4(a_position, 0.0, 1.0); gl_Position = vec4(a_position, 0.0, 1.0);
} }
`; `.trim();
const fsOverlay = `#version 300 es const fsOverlay = `#version 300 es
precision mediump float; precision mediump float;
@@ -101,7 +100,85 @@ void main() {
if (color.a < 0.01) discard; if (color.a < 0.01) discard;
outColor = color; 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 --- // --- WEBGL STATE ---
let matrixProgram, overlayProgram; let matrixProgram, overlayProgram;
let instanceBuffer, quadBuffer, overlayQuadBuffer; let instanceBuffer, quadBuffer, overlayQuadBuffer;
@@ -209,7 +286,19 @@ function initWebGL() {
atlasTexture = gl.createTexture(); atlasTexture = gl.createTexture();
overlayTexture = 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.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.enable(gl.BLEND); gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
@@ -239,10 +328,6 @@ function buildAtlas() {
b = 199; b = 199;
} }
const bgR = isDarkMode ? 10 : 250;
const bgG = isDarkMode ? 10 : 250;
const bgB = isDarkMode ? 12 : 252;
atlasCtx.font = `bold ${fontSize}px monospace`; atlasCtx.font = `bold ${fontSize}px monospace`;
atlasCtx.textBaseline = "top"; atlasCtx.textBaseline = "top";
atlasCtx.textAlign = "center"; atlasCtx.textAlign = "center";
@@ -251,8 +336,8 @@ function buildAtlas() {
for (let row = 0; row <= 9; row++) { for (let row = 0; row <= 9; row++) {
const alpha = (row + 1) / 10; const alpha = (row + 1) / 10;
const yOffset = row * fontSize; const yOffset = row * fontSize;
atlasCtx.fillStyle = `rgba(${bgR}, ${bgG}, ${bgB}, ${alpha})`;
atlasCtx.fillRect(0, yOffset, atlasCanvas.width, fontSize); // УБРАЛИ fillRect с серым фоном. Оставляем ТОЛЬКО цветной текст!
atlasCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`; atlasCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
for (let i = 0; i < chars.length; i++) { for (let i = 0; i < chars.length; i++) {
atlasCtx.fillText(chars[i], i * fontSize + halfFs, yOffset); atlasCtx.fillText(chars[i], i * fontSize + halfFs, yOffset);
@@ -260,8 +345,7 @@ function buildAtlas() {
} }
const headY = 10 * fontSize; const headY = 10 * fontSize;
atlasCtx.fillStyle = `rgb(${bgR}, ${bgG}, ${bgB})`; // УБРАЛИ fillRect для "головы"
atlasCtx.fillRect(0, headY, atlasCanvas.width, fontSize);
atlasCtx.fillStyle = "rgb(255, 255, 255)"; atlasCtx.fillStyle = "rgb(255, 255, 255)";
for (let i = 0; i < chars.length; i++) { for (let i = 0; i < chars.length; i++) {
atlasCtx.fillText(chars[i], i * fontSize + halfFs, headY); atlasCtx.fillText(chars[i], i * fontSize + halfFs, headY);
@@ -327,7 +411,7 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
const eyeWidth = (120 * (1 - isClosed) + 140 * isClosed) * eyeScale; const eyeWidth = (120 * (1 - isClosed) + 140 * isClosed) * eyeScale;
const eyeRadius = (36 * (1 - isClosed) + 8 * isClosed) * eyeScale; const eyeRadius = (36 * (1 - isClosed) + 8 * isClosed) * eyeScale;
const laserWidth = 140 * isClosed * eyeScale; const laserWidth = 140 * isClosed * eyeScale;
const blurMultiplier = isMobile ? 0 : 1.0; const blurMultiplier = isMobile ? 0 : isDarkMode ? 1.0 : 0.1;
ctx.save(); ctx.save();
ctx.fillStyle = bgFill; ctx.fillStyle = bgFill;
@@ -342,7 +426,6 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
ctx.fill(); ctx.fill();
ctx.strokeStyle = color; ctx.strokeStyle = color;
// Толщина линии не зависит от масштаба, остается тонкой
ctx.lineWidth = 3.5 * (1 - isClosed) + 2.5 * isClosed; ctx.lineWidth = 3.5 * (1 - isClosed) + 2.5 * isClosed;
ctx.shadowColor = color; ctx.shadowColor = color;
ctx.shadowBlur = (12 * (1 - isClosed) + 6 * isClosed) * blurMultiplier; ctx.shadowBlur = (12 * (1 - isClosed) + 6 * isClosed) * blurMultiplier;
@@ -375,8 +458,9 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
ctx.stroke(); ctx.stroke();
ctx.restore(); ctx.restore();
ctx.strokeStyle = isDarkMode ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.3)"; // ИСПРАВЛЕНИЕ: Делаем зрачок и перекрестие ЖЕСТКО черным в светлой теме
ctx.lineWidth = 1; 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.globalAlpha = alphaMult;
ctx.beginPath(); ctx.beginPath();
ctx.arc(0, 0, 10 * eyeScale, 0, Math.PI * 2); ctx.arc(0, 0, 10 * eyeScale, 0, Math.PI * 2);
@@ -396,7 +480,8 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
ctx.lineTo(-5 * eyeScale, 0); ctx.lineTo(-5 * eyeScale, 0);
ctx.fill(); ctx.fill();
ctx.fillStyle = color; // Центральная точка
ctx.fillStyle = isDarkMode ? color : "rgba(0,0,0,0.9)";
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
ctx.beginPath(); ctx.beginPath();
ctx.arc(0, 0, 1.5 * eyeScale, 0, Math.PI * 2); ctx.arc(0, 0, 1.5 * eyeScale, 0, Math.PI * 2);
@@ -517,7 +602,6 @@ function drawOverlayElements() {
return hasContent; return hasContent;
} }
// --- ГЛАВНЫЙ ЦИКЛ ОТРИСОВКИ --- // --- ГЛАВНЫЙ ЦИКЛ ОТРИСОВКИ ---
function render(currentTime) { function render(currentTime) {
requestAnimationFrame(render); requestAnimationFrame(render);
@@ -527,8 +611,18 @@ function render(currentTime) {
if (now - lastDrawTime < 16) return; if (now - lastDrawTime < 16) return;
lastDrawTime = now; lastDrawTime = now;
engine.tick(); 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); gl.viewport(0, 0, width * pixelScale, height * pixelScale);
const bgR = isDarkMode ? 10 / 255 : 250 / 255; const bgR = isDarkMode ? 10 / 255 : 250 / 255;
@@ -537,10 +631,9 @@ function render(currentTime) {
gl.clearColor(bgR, bgG, bgB, 1.0); gl.clearColor(bgR, bgG, bgB, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
// 1. РЕНДЕР МАТРИЦЫ // 1.1. РЕНДЕР МАТРИЦЫ
const renderLen = engine.render_len(); const renderLen = engine.render_len();
const instanceCount = renderLen / 5; const instanceCount = renderLen / 5;
if (instanceCount > 0) { if (instanceCount > 0) {
gl.useProgram(matrixProgram); gl.useProgram(matrixProgram);
gl.bindVertexArray(vaoMatrix); gl.bindVertexArray(vaoMatrix);
@@ -557,7 +650,6 @@ function render(currentTime) {
engine.render_ptr(), engine.render_ptr(),
renderLen, renderLen,
); );
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
if (renderLen > instanceBufferCapacity) { if (renderLen > instanceBufferCapacity) {
instanceBufferCapacity = Math.max(renderLen * 2, 20000); instanceBufferCapacity = Math.max(renderLen * 2, 20000);
@@ -568,20 +660,17 @@ function render(currentTime) {
); );
} }
gl.bufferSubData(gl.ARRAY_BUFFER, 0, renderDataView); gl.bufferSubData(gl.ARRAY_BUFFER, 0, renderDataView);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount); gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount);
} }
// 2. РЕНДЕР ОВЕРЛЕЯ // 1.2. РЕНДЕР ОВЕРЛЕЯ (Глаза, глитчи)
if (drawOverlayElements()) { if (drawOverlayElements()) {
gl.useProgram(overlayProgram); gl.useProgram(overlayProgram);
gl.bindVertexArray(vaoOverlay); gl.bindVertexArray(vaoOverlay);
// Обновляем текстуру из 2D Canvas
gl.activeTexture(gl.TEXTURE1); gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, overlayTexture); gl.bindTexture(gl.TEXTURE_2D, overlayTexture);
// Для overlay используем обычную загрузку текстуры
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D( gl.texImage2D(
gl.TEXTURE_2D, gl.TEXTURE_2D,
0, 0,
@@ -593,11 +682,48 @@ function render(currentTime) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 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_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, 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.uniform1i(gl.getUniformLocation(overlayProgram, "u_texture"), 1);
gl.drawArrays(gl.TRIANGLES, 0, 6); 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 --- // --- ОБРАБОТЧИК СООБЩЕНИЙ ОТ REACT ---
@@ -612,8 +738,9 @@ self.onmessage = async (e) => {
width = payload.logicalWidth; width = payload.logicalWidth;
height = payload.logicalHeight; height = payload.logicalHeight;
pixelScale = payload.pixelScale; pixelScale = payload.pixelScale;
eyeScale = payload.eyeScale || 0.8;
isMobile = payload.isMobile; isMobile = payload.isMobile;
isLowQuality = payload.isLowQuality;
eyeScale = isMobile ? 1.2 : 1.5;
isVpnOn = payload.isVpnOn; isVpnOn = payload.isVpnOn;
isDarkMode = payload.isDarkMode; isDarkMode = payload.isDarkMode;
fontSize = payload.fontSize || 16; fontSize = payload.fontSize || 16;
@@ -626,6 +753,30 @@ self.onmessage = async (e) => {
overlayCtx.scale(pixelScale, pixelScale); overlayCtx.scale(pixelScale, pixelScale);
initWebGL(); 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(); buildAtlas();
try { try {
@@ -648,6 +799,7 @@ self.onmessage = async (e) => {
// Запускаем главный цикл // Запускаем главный цикл
requestAnimationFrame(render); requestAnimationFrame(render);
self.postMessage({ type: "READY" });
} catch (err) { } catch (err) {
console.error("Critical failure loading WASM core in Worker:", err); console.error("Critical failure loading WASM core in Worker:", err);
} }
@@ -656,8 +808,11 @@ self.onmessage = async (e) => {
width = payload.logicalWidth; width = payload.logicalWidth;
height = payload.logicalHeight; height = payload.logicalHeight;
pixelScale = payload.pixelScale; pixelScale = payload.pixelScale;
eyeScale = payload.eyeScale || 0.8;
fontSize = payload.fontSize || 16; fontSize = payload.fontSize || 16;
isLowQuality = payload.isLowQuality;
gl.canvas.width = width * pixelScale;
gl.canvas.height = height * pixelScale;
overlayCanvas.width = width * pixelScale; overlayCanvas.width = width * pixelScale;
overlayCanvas.height = height * pixelScale; overlayCanvas.height = height * pixelScale;
@@ -665,15 +820,47 @@ self.onmessage = async (e) => {
if (typeof engine.set_mobile === "function") if (typeof engine.set_mobile === "function")
engine.set_mobile(payload.isMobile); engine.set_mobile(payload.isMobile);
eyeScale = isMobile ? 1.2 : 1.5;
engine.resize(width, height); 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") if (payload.eyeY && typeof engine.set_eye_anchor === "function")
engine.set_eye_anchor(payload.eyeY); engine.set_eye_anchor(payload.eyeY);
buildAtlas(); buildAtlas();
} else if (type === "THEME") { } else if (type === "THEME") {
if (!engine) return; // Сохраняем статус безусловно!
isVpnOn = payload.isVpnOn; isVpnOn = payload.isVpnOn;
isDarkMode = payload.isDarkMode; 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") if (typeof engine.set_vpn_status === "function")
engine.set_vpn_status(isVpnOn); engine.set_vpn_status(isVpnOn);
buildAtlas(); buildAtlas();
+6 -8
View File
@@ -223,11 +223,10 @@ impl MatrixEngine {
fn init_drops(&mut self) { fn init_drops(&mut self) {
self.drops.clear(); self.drops.clear();
// Ускоренный в 2 раза дождь:
let layers = [ let layers = [
(0, 0.4, 0.01, 0.04), // Far: было 0.005-0.02 (0, 0.4, 0.005, 0.02), // Far
(1, 0.7, 0.04, 0.12), // Mid: было 0.02-0.06 (1, 0.7, 0.02, 0.06), // Mid
(2, 1.0, 0.10, 0.30), // Near: было 0.05-0.15 (2, 1.0, 0.05, 0.15), // Near
]; ];
let base_spacing = self.font_size as f32; let base_spacing = self.font_size as f32;
@@ -406,8 +405,9 @@ impl MatrixEngine {
if self.blink_timer > 0.0 { if self.blink_timer > 0.0 {
self.blink_timer -= 1.0; self.blink_timer -= 1.0;
} else if !self.is_vpn_on && self.rng.gen_bool(0.0006) { } else if !self.is_vpn_on && self.rng.gen_bool(0.006) {
self.blink_timer = 8.0; // Шанс моргнуть стал 0.6% каждый кадр
self.blink_timer = 12.0; // Само закрытие длится чуть дольше
} }
let target_closedness = if self.is_vpn_on || self.blink_timer > 0.0 { let target_closedness = if self.is_vpn_on || self.blink_timer > 0.0 {
@@ -472,8 +472,6 @@ impl MatrixEngine {
_ => 1.0, _ => 1.0,
}; };
// Гироскоп и бесконечный экран по горизонтали
drop.base_x += self.tilt_x * parallax_factor * 5.0;
let wrap_margin = 150.0; let wrap_margin = 150.0;
if drop.base_x < -wrap_margin { if drop.base_x < -wrap_margin {
drop.base_x += self.width as f32 + wrap_margin * 2.0; drop.base_x += self.width as f32 + wrap_margin * 2.0;