/** * Основной файл Воркера. * Использует импорты ES6 (требует type: "module" при создании воркера). */ 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 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 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; let MatrixEngineClass; 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"); 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, width * currentPixelScale, height * currentPixelScale, 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) { 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, width * currentPixelScale, height * currentPixelScale); 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); 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) { 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.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_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; initWebGL(); 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, ); 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(); requestAnimationFrame(render); self.postMessage({ type: "READY" }); } catch (err) { console.error("Critical failure loading WASM core:", err); } } 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") { 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); } };