scales fixes, rubbish remove

This commit is contained in:
2026-04-26 21:19:01 +07:00
parent 25f3783f02
commit 8a41a79be2
4 changed files with 101 additions and 165 deletions
+12
View File
@@ -202,3 +202,15 @@
background: #333; background: #333;
} }
} }
#matrix-canvas {
display: block;
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: -1; /* Чтобы фон был ЗА контентом */
pointer-events: auto; /* Чтобы мышка и свайпы работали */
background-color: #0a0a0c; /* Твой темный цвет из шейдера, чтобы не было белых вспышек */
}
+44 -18
View File
@@ -60,8 +60,23 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
if (!container || hasTransferred.current) return; if (!container || hasTransferred.current) return;
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.className =
"absolute inset-0 w-full h-full block transition-opacity duration-1000 opacity-0 z-0"; // Используем zIndex: -1, чтобы канвас был ФОНОМ внутри контейнера
Object.assign(canvas.style, {
position: "absolute",
top: "0",
left: "0",
width: "100%",
height: "100%",
display: "block",
zIndex: "-1",
backgroundColor: isDarkMode ? "#0a0a0c" : "#fafafc",
pointerEvents: "none", // Чтобы клики проходили сквозь канвас на кнопки
transition: "opacity 1.5s ease-in-out",
opacity: "0",
});
canvas.id = "matrix-canvas";
container.insertBefore(canvas, container.firstChild); container.insertBefore(canvas, container.firstChild);
try { try {
@@ -73,15 +88,19 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
const isWeakHardware = cores <= 4 || memory <= 4; const isWeakHardware = cores <= 4 || memory <= 4;
const isLowQuality = isMobile || isWeakHardware; const isLowQuality = isMobile || isWeakHardware;
// На слабом ПК и телефонах рендерим 75% от разрешения экрана const isMobileDevice = window.innerWidth <= 768;
const pixelScale = 1;
const currentDPR = window.devicePixelRatio || 1;
const optimalScale = isMobileDevice
? Math.min(currentDPR, 1.25)
: currentDPR;
// ---------------------------------------------- // ----------------------------------------------
const logicalWidth = container.clientWidth; const logicalWidth = container.clientWidth;
const logicalHeight = container.clientHeight; const logicalHeight = container.clientHeight;
canvas.width = logicalWidth * pixelScale; canvas.width = logicalWidth * optimalScale;
canvas.height = logicalHeight * pixelScale; canvas.height = logicalHeight * optimalScale;
const worker = new Worker( const worker = new Worker(
`/wasm-matrix/matrix_worker.js?v=${ENGINE_VERSION}`, `/wasm-matrix/matrix_worker.js?v=${ENGINE_VERSION}`,
@@ -100,7 +119,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
canvas: offscreen, canvas: offscreen,
logicalWidth, logicalWidth,
logicalHeight, logicalHeight,
pixelScale, pixelScale: optimalScale,
eyeY: getEyeY(), eyeY: getEyeY(),
eyeScale, eyeScale,
isMobile, isMobile,
@@ -115,26 +134,32 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
worker.onmessage = (e) => { worker.onmessage = (e) => {
if (e.data.type === "READY") { if (e.data.type === "READY") {
canvas.classList.remove("opacity-0"); // Небольшой таймаут гарантирует, что браузер применит transition
canvas.classList.add("opacity-100"); requestAnimationFrame(() => {
canvas.style.opacity = "1";
setIsLoaded(true);
});
} }
}; };
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: newWidth, height: newHeight } = entry.contentRect;
if (width === 0 || height === 0) continue; if (newWidth === 0 || newHeight === 0) continue;
const isMob = width <= 768; const isMob = newWidth <= 768;
const lowQual = isMob || isWeakHardware; const lowQual = isMob || isWeakHardware;
const pScale = 1;
// ВАЖНО: Используем тот же коэффициент, что и при INIT
const currentDPR = window.devicePixelRatio || 1;
const pScale = isMob ? Math.min(currentDPR, 1.25) : currentDPR;
workerRef.current?.postMessage({ workerRef.current?.postMessage({
type: "RESIZE", type: "RESIZE",
payload: { payload: {
logicalWidth: width, logicalWidth: newWidth,
logicalHeight: height, logicalHeight: newHeight,
pixelScale: pScale, pixelScale: pScale, // Теперь скейл совпадает с физикой канваса
eyeY: getEyeY(), eyeY: getEyeY(),
eyeScale: isMob ? 0.8 : 1.5, eyeScale: isMob ? 0.8 : 1.5,
isMobile: isMob, isMobile: isMob,
@@ -144,7 +169,6 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
}); });
} }
}); });
resizeObserver.observe(container); resizeObserver.observe(container);
return () => { return () => {
@@ -266,9 +290,10 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className="absolute inset-0 overflow-hidden z-0" className="absolute inset-0 overflow-hidden z-5" // Убрали z-0, чтобы не путаться
style={{ backgroundColor: isDarkMode ? "#0a0a0c" : "#fafafc" }} style={{ backgroundColor: isDarkMode ? "#0a0a0c" : "#fafafc" }}
> >
{/* Оверлей со скан-линиями. Должен быть ВЫШЕ канваса (z-10) */}
<div <div
className="absolute inset-0 pointer-events-none opacity-[0.15] dark:opacity-[0.25] hidden md:block z-10" className="absolute inset-0 pointer-events-none opacity-[0.15] dark:opacity-[0.25] hidden md:block z-10"
style={{ style={{
@@ -276,6 +301,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
backgroundSize: "100% 4px, 3px 100%", backgroundSize: "100% 4px, 3px 100%",
}} }}
/> />
{/* Канвас вставится сюда программно с zIndex -1 */}
</div> </div>
); );
}; };
+27 -138
View File
@@ -6,7 +6,6 @@
let engine, wasm; let engine, wasm;
let gl; let gl;
let overlayCanvas, overlayCtx;
let atlasCanvas, atlasCtx, atlasTexture; let atlasCanvas, atlasCtx, atlasTexture;
let width, height; let width, height;
let fontSize = 16; let fontSize = 16;
@@ -286,7 +285,6 @@ let matrixProgram, overlayProgram;
let instanceBuffer, quadBuffer, overlayQuadBuffer; let instanceBuffer, quadBuffer, overlayQuadBuffer;
let vaoMatrix, vaoOverlay; let vaoMatrix, vaoOverlay;
let uResLoc, uFontLoc, uAtlasSizeLoc; let uResLoc, uFontLoc, uAtlasSizeLoc;
let overlayTexture;
let instanceBufferCapacity = 0; let instanceBufferCapacity = 0;
// ДОБАВЛЕННЫЕ ПЕРЕМЕННЫЕ ДЛЯ ГЛИТЧЕЙ И ГЛАЗ // ДОБАВЛЕННЫЕ ПЕРЕМЕННЫЕ ДЛЯ ГЛИТЧЕЙ И ГЛАЗ
@@ -739,101 +737,6 @@ function drawEye(ctx, cx, cy, gazeX, gazeY, dir, isClosed) {
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) { function render(currentTime) {
requestAnimationFrame(render); requestAnimationFrame(render);
if (!engine || !wasm || !gl) return; if (!engine || !wasm || !gl) return;
@@ -1047,10 +950,15 @@ self.onmessage = async (e) => {
gl = payload.canvas.getContext("webgl2", { gl = payload.canvas.getContext("webgl2", {
alpha: false, alpha: false,
antialias: false, antialias: false,
powerPreference: "high-performance",
}); });
width = payload.logicalWidth; width = payload.logicalWidth;
height = payload.logicalHeight; height = payload.logicalHeight;
pixelScale = payload.pixelScale; pixelScale = payload.pixelScale;
maxPixelScale = pixelScale; // Сохраняем максимум для авто-скейлера
currentPixelScale = pixelScale;
isMobile = payload.isMobile; isMobile = payload.isMobile;
isLowQuality = payload.isLowQuality; isLowQuality = payload.isLowQuality;
eyeScale = isMobile ? 1.2 : 1.5; eyeScale = isMobile ? 1.2 : 1.5;
@@ -1058,22 +966,17 @@ self.onmessage = async (e) => {
isDarkMode = payload.isDarkMode; isDarkMode = payload.isDarkMode;
fontSize = payload.fontSize || 16; fontSize = payload.fontSize || 16;
overlayCanvas = new OffscreenCanvas( // Инициализируем шейдеры и буферы
width * pixelScale,
height * pixelScale,
);
overlayCtx = overlayCanvas.getContext("2d", { alpha: true });
overlayCtx.scale(pixelScale, pixelScale);
initWebGL(); initWebGL();
// Настройка главной текстуры под текущее разрешение
gl.bindTexture(gl.TEXTURE_2D, postTexture); gl.bindTexture(gl.TEXTURE_2D, postTexture);
gl.texImage2D( gl.texImage2D(
gl.TEXTURE_2D, gl.TEXTURE_2D,
0, 0,
gl.RGBA, gl.RGBA,
width * pixelScale, width * currentPixelScale,
height * pixelScale, height * currentPixelScale,
0, 0,
gl.RGBA, gl.RGBA,
gl.UNSIGNED_BYTE, gl.UNSIGNED_BYTE,
@@ -1088,7 +991,6 @@ self.onmessage = async (e) => {
postTexture, postTexture,
0, 0,
); );
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
buildAtlas(); buildAtlas();
@@ -1096,72 +998,59 @@ self.onmessage = async (e) => {
const module = await import("./matrix_engine.js?v=" + version); const module = await import("./matrix_engine.js?v=" + version);
const init = module.default; const init = module.default;
MatrixEngineClass = module.MatrixEngine; MatrixEngineClass = module.MatrixEngine;
// Передаем объект для инициализации (исправление warning)
wasm = await init({ wasm = await init({
module_or_path: "./matrix_engine_bg.wasm?v=" + version, module_or_path: "./matrix_engine_bg.wasm?v=" + version,
}); });
engine = new MatrixEngineClass(width, height, fontSize); engine = new MatrixEngineClass(width, height, fontSize);
if (typeof engine.set_mobile === "function") engine.set_mobile(isMobile); if (typeof engine.set_mobile === "function") engine.set_mobile(isMobile);
if (typeof engine.set_vpn_status === "function") if (typeof engine.set_vpn_status === "function")
engine.set_vpn_status(isVpnOn); engine.set_vpn_status(isVpnOn);
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);
// Запускаем главный цикл
requestAnimationFrame(render); requestAnimationFrame(render);
self.postMessage({ type: "READY" }); 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:", err);
} }
} else if (type === "RESIZE") { } else if (type === "RESIZE") {
if (!engine) return; if (!engine) return;
width = payload.logicalWidth; width = payload.logicalWidth;
height = payload.logicalHeight; height = payload.logicalHeight;
pixelScale = payload.pixelScale; pixelScale = payload.pixelScale; // Новое базовое значение DPR
maxPixelScale = pixelScale;
currentPixelScale = pixelScale;
fontSize = payload.fontSize || 16; fontSize = payload.fontSize || 16;
isLowQuality = payload.isLowQuality; isLowQuality = payload.isLowQuality;
isMobile = payload.isMobile;
gl.canvas.width = width * pixelScale; // 1. Обновляем физический размер холста
gl.canvas.height = height * pixelScale; gl.canvas.width = width * currentPixelScale;
gl.canvas.height = height * currentPixelScale;
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);
// 2. Обновляем размер текстуры постобработки
gl.bindTexture(gl.TEXTURE_2D, postTexture); gl.bindTexture(gl.TEXTURE_2D, postTexture);
gl.texImage2D( gl.texImage2D(
gl.TEXTURE_2D, gl.TEXTURE_2D,
0, 0,
gl.RGBA, gl.RGBA,
width * pixelScale, gl.canvas.width,
height * pixelScale, gl.canvas.height,
0, 0,
gl.RGBA, gl.RGBA,
gl.UNSIGNED_BYTE, gl.UNSIGNED_BYTE,
null, null,
); );
gl.bindFramebuffer(gl.FRAMEBUFFER, postFramebuffer); // 3. Сбрасываем вьюпорт WebGL
gl.framebufferTexture2D( gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0, eyeScale = isMobile ? 1.2 : 1.5;
gl.TEXTURE_2D, engine.resize(width, height);
postTexture, if (typeof engine.set_mobile === "function") engine.set_mobile(isMobile);
0,
);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
if (payload.eyeY && typeof engine.set_eye_anchor === "function")
engine.set_eye_anchor(payload.eyeY);
buildAtlas(); buildAtlas();
} else if (type === "THEME") { } else if (type === "THEME") {
// Сохраняем статус безусловно! // Сохраняем статус безусловно!
+18 -9
View File
@@ -230,19 +230,25 @@ impl MatrixEngine {
fn init_drops(&mut self) { fn init_drops(&mut self) {
self.drops.clear(); self.drops.clear();
let layers = [ // Используем Vec вместо статического массива, чтобы динамически добавлять слои
(0, 0.4, 0.005, 0.02), // Far let mut layers = vec![
(1, 0.7, 0.02, 0.06), // Mid (1, 0.7, 0.02, 0.06), // Mid (Средний слой)
(2, 1.0, 0.05, 0.15), // Near (2, 1.0, 0.05, 0.15), // Near (Ближний слой)
]; ];
// Добавляем дальний размытый слой ТОЛЬКО для ПК
if !self.is_mobile {
layers.insert(0, (0, 0.4, 0.005, 0.02)); // Far (Дальний слой)
}
let base_spacing = self.font_size as f32; let base_spacing = self.font_size as f32;
for (layer, scale, min_spd, max_spd) in layers.iter() { for (layer, scale, min_spd, max_spd) in layers.iter() {
let spacing = base_spacing * scale; let spacing = base_spacing * scale;
// Формула колонок (с твоей предыдущей оптимизацией для мобилок)
let columns = if self.is_mobile { let columns = if self.is_mobile {
(self.width as f32 / spacing).ceil() as u32 + 5 // Меньше запасных колонок (self.width as f32 / spacing).ceil() as u32 + 5
} else { } else {
(self.width as f32 / spacing).ceil() as u32 + 20 (self.width as f32 / spacing).ceil() as u32 + 20
}; };
@@ -264,7 +270,6 @@ impl MatrixEngine {
// Stride 5: [x, y, char_idx, atlas_row, scale] // Stride 5: [x, y, char_idx, atlas_row, scale]
self.instance_buffer.resize(self.drops.len() * 5, 0.0); self.instance_buffer.resize(self.drops.len() * 5, 0.0);
} }
pub fn resize(&mut self, width: u32, height: u32) { pub fn resize(&mut self, width: u32, height: u32) {
self.width = width; self.width = width;
self.height = height; self.height = height;
@@ -533,7 +538,7 @@ impl MatrixEngine {
let dist = (dx * dx + dy * dy).sqrt(); let dist = (dx * dx + dy * dy).sqrt();
if (dist - sw.radius).abs() < 40.0 { if (dist - sw.radius).abs() < 40.0 {
let base_sw_force = if self.is_mobile { 6.0 } else { 18.0 }; // На мобилке импульс в 3 раза слабее let base_sw_force = if self.is_mobile { 3.0 } else { 18.0 }; // На мобилке импульс в 3 раза слабее
let force = (sw.life / sw.max_life) * base_sw_force * parallax_factor; let force = (sw.life / sw.max_life) * base_sw_force * parallax_factor;
drop.vel_x += (dx / dist.max(1.0_f32)) * force; drop.vel_x += (dx / dist.max(1.0_f32)) * force;
force_white = true; force_white = true;
@@ -551,12 +556,16 @@ impl MatrixEngine {
let dx = current_x - ix; let dx = current_x - ix;
let dy = current_y - iy; let dy = current_y - iy;
let dist_sq = dx * dx + dy * dy; let dist_sq = dx * dx + dy * dy;
let rad = if self.is_mobile { 100.0 } else { 140.0 }; let rad = if self.is_mobile { 70.0 } else { 140.0 }; // Радиус захвата в 2 раза меньше
if dist_sq < rad * rad { if dist_sq < rad * rad {
let dist = dist_sq.sqrt().max(1.0_f32); let dist = dist_sq.sqrt().max(1.0_f32);
let force = (rad - dist) / rad; let force = (rad - dist) / rad;
drop.vel_x += (dx / dist) * force * 15.0 * parallax_factor;
// ДОБАВЛЯЕМ ДИНАМИЧЕСКУЮ СИЛУ (на телефоне в 4 раза слабее)
let interaction_force = if self.is_mobile { 3.5 } else { 15.0 };
drop.vel_x += (dx / dist) * force * interaction_force * parallax_factor;
drop.y -= drop.speed * force * 0.6; drop.y -= drop.speed * force * 0.6;
is_scrambled = true; is_scrambled = true;
if dist < rad * 0.5 { if dist < rad * 0.5 {