721 lines
36 KiB
JavaScript
721 lines
36 KiB
JavaScript
/**
|
|
* Файл содержит все исходные коды шейдеров для проекта Matrix.
|
|
* Разделение помогает фокусироваться на графике отдельно от логики.
|
|
*/
|
|
|
|
/**
|
|
* Вершинный шейдер капель матричного дождя. Рисует каждую каплю как
|
|
* инстансированный quad (2 треугольника, позиция берётся из a_position —
|
|
* см. STRIDE=5 в lib.rs и раскладку атрибутов в initWebGL()). Неочевидное:
|
|
* - UV-координаты в атлас (v_uv) считаются прямо здесь из a_charIdx и
|
|
* размера шрифта, а не передаются готовыми — атлас "нарезается" по
|
|
* формуле char_idx * fontSize по X, конкретная строка добавляется уже во
|
|
* фрагментном шейдере через a_atlasRow.
|
|
* - Лёгкое "притяжение" символов к курсору (pushOffset/hoverScale): каждый
|
|
* инстанс сам проверяет расстояние до u_mouse в clip-пространстве с учётом
|
|
* аспекта экрана и слегка сдвигается/увеличивается — это чисто визуальный
|
|
* эффект на уровне вершин, независимый от физики капель в WASM.
|
|
*/
|
|
export const vsMatrix = `#version 300 es
|
|
precision mediump float;
|
|
in vec2 a_quad;
|
|
in vec2 a_position;
|
|
in float a_charIdx;
|
|
in float a_atlasRow;
|
|
in float a_scale;
|
|
|
|
uniform vec2 u_resolution;
|
|
uniform float u_fontSize;
|
|
uniform vec2 u_mouse;
|
|
|
|
out vec2 v_uv;
|
|
out float v_atlasRow;
|
|
out float v_dist;
|
|
|
|
void main() {
|
|
float size = u_fontSize * a_scale;
|
|
|
|
vec2 res = max(u_resolution, vec2(1.0));
|
|
vec2 centerPos = a_position + vec2(size * 0.5);
|
|
vec2 zeroToOneCenter = centerPos / res;
|
|
vec2 aspect = vec2(res.x / res.y, 1.0);
|
|
|
|
vec2 diff = zeroToOneCenter * aspect - u_mouse * aspect;
|
|
float dist = length(diff);
|
|
v_dist = dist;
|
|
|
|
vec2 pushOffset = vec2(0.0);
|
|
float hoverScale = 1.0;
|
|
|
|
if (u_mouse.x > 0.001 && u_mouse.y > 0.001 && dist < 0.15 && dist > 0.0001) {
|
|
float force = 1.0 - smoothstep(0.0, 0.15, dist);
|
|
vec2 dir = diff / dist;
|
|
pushOffset = dir * force * 40.0;
|
|
hoverScale = 1.0 + force * 0.8;
|
|
}
|
|
|
|
vec2 finalPos = a_position + pushOffset + a_quad * (size * hoverScale);
|
|
|
|
vec2 zeroToOne = finalPos / res;
|
|
vec2 zeroToTwo = zeroToOne * 2.0;
|
|
vec2 clipSpace = zeroToTwo - 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_atlasRow = a_atlasRow;
|
|
}`.trim();
|
|
|
|
/**
|
|
* Вершинный шейдер полноэкранного прохода пост-процессинга. Рисует один
|
|
* quad на весь clip space (-1..1) без всякой трансформации — вся работа
|
|
* пост-процесса происходит во фрагментном шейдере (postProcessSource),
|
|
* этот шейдер лишь превращает clip-координаты в UV (0..1) для выборки из
|
|
* промежуточной текстуры postTexture.
|
|
*/
|
|
export const vsOverlay = `#version 300 es
|
|
precision mediump float;
|
|
in vec2 a_position;
|
|
out vec2 v_uv;
|
|
void main() {
|
|
v_uv = a_position * 0.5 + 0.5;
|
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
}`.trim();
|
|
|
|
/**
|
|
* Вершинный шейдер глитч-блоков (VHS-подобные цветные прямоугольники,
|
|
* см. GlitchBlock в lib.rs, буфер stride=5: x, y, w, h, color_type).
|
|
* Неочевидное: newSize = originalSize * 3.0 — геометрия рисуется втрое
|
|
* крупнее размеров w/h, посчитанных в Rust, а сдвиг offset центрирует
|
|
* увеличенный quad вокруг исходной позиции. Так мелкие "осколки" из lib.rs
|
|
* визуально превращаются в заметные, хорошо читаемые полосы-глитчи без
|
|
* необходимости считать увеличенный размер уже на стороне симуляции.
|
|
*/
|
|
export const vsGlitch = `#version 300 es
|
|
in vec2 a_quad;
|
|
in vec4 a_glitchData;
|
|
in float a_colorType;
|
|
uniform vec2 u_resolution;
|
|
out float v_colorType;
|
|
void main() {
|
|
vec2 originalSize = vec2(a_glitchData.z, a_glitchData.w);
|
|
vec2 newSize = originalSize * 3.0;
|
|
vec2 offset = (newSize - originalSize) * 0.5;
|
|
vec2 pixelPos = vec2(a_glitchData.x, a_glitchData.y) - offset + a_quad * newSize;
|
|
vec2 zeroToOne = pixelPos / u_resolution;
|
|
vec2 clipSpace = (zeroToOne * 2.0) - 1.0;
|
|
gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);
|
|
v_colorType = a_colorType;
|
|
}`.trim();
|
|
|
|
/**
|
|
* Фрагментный шейдер капель. Выбирает пиксель из текстурного атласа символов
|
|
* по v_uv (X внутри символа/колонки) и v_atlasRow (какая из 11 строк —
|
|
* градация яркости 0..9 или "белая голова" 10, см. buildAtlas() в
|
|
* matrix_worker.js), затем discard-ит полностью прозрачные пиксели (пустое
|
|
* пространство вокруг глифа в атласе), чтобы не тратить fill rate и не
|
|
* ломать блендинг перекрывающихся капель. Дополнительно перекрашивает
|
|
* символы рядом с курсором (v_dist из вершинного шейдера) в акцентный цвет.
|
|
*/
|
|
export const fsMatrix = `#version 300 es
|
|
precision mediump float;
|
|
in vec2 v_uv;
|
|
in float v_atlasRow;
|
|
in float v_dist;
|
|
|
|
uniform sampler2D u_atlas;
|
|
uniform float u_fontSize;
|
|
uniform vec2 u_atlasSize;
|
|
uniform vec2 u_mouse;
|
|
|
|
out vec4 outColor;
|
|
|
|
void main() {
|
|
float yOffset = v_atlasRow * u_fontSize;
|
|
vec2 finalUV = vec2(v_uv.x, v_uv.y + yOffset) / max(u_atlasSize, vec2(1.0));
|
|
vec4 texColor = texture(u_atlas, finalUV);
|
|
|
|
if (texColor.a < 0.001) discard;
|
|
|
|
if (u_mouse.x > 0.001 && u_mouse.y > 0.001 && v_dist < 0.15) {
|
|
float intensity = 1.0 - smoothstep(0.0, 0.15, v_dist);
|
|
vec3 targetColor = vec3(0.7, 0.0, 1.0);
|
|
texColor.rgb = mix(texColor.rgb, targetColor * texColor.a, intensity);
|
|
}
|
|
|
|
outColor = texColor;
|
|
}`.trim();
|
|
|
|
/**
|
|
* Фрагментный шейдер глитч-блоков. Цвет блока целиком определяется
|
|
* v_colorType (0..4), переданным из Rust (GlitchBlock::color_type,
|
|
* распределение см. в GlitchBlock::new): фон темы, основной цвет темы,
|
|
* инвертированный цвет, чистый белый и акцентный "красный сбой" —
|
|
* никакой процедурной генерации цвета здесь нет, только ветвление по типу.
|
|
*/
|
|
export const fsGlitch = `#version 300 es
|
|
precision mediump float;
|
|
in float v_colorType;
|
|
uniform vec3 u_themeMain;
|
|
uniform vec3 u_themeBg;
|
|
out vec4 outColor;
|
|
void main() {
|
|
int type = int(v_colorType + 0.1);
|
|
if (type == 0) { outColor = vec4(u_themeBg, 1.0); }
|
|
else if (type == 1) { outColor = vec4(u_themeMain, 0.9); }
|
|
else if (type == 2) { outColor = vec4(vec3(1.0) - u_themeMain, 0.7); }
|
|
else if (type == 3) { outColor = vec4(1.0, 1.0, 1.0, 0.95); }
|
|
else if (type == 4) { outColor = vec4(1.0, 0.08, 0.31, 0.95); }
|
|
else { discard; }
|
|
}`.trim();
|
|
|
|
/**
|
|
* Фоновый проход глубины: рисуется ПЕРВЫМ внутри postFramebuffer, до капель
|
|
* дождя (см. render() в matrix_worker.js) — минималистичная синтвейв-сцена
|
|
* (небо, силуэты города, дорога с перспективой к линии горизонта и машины,
|
|
* едущие в противоположных полосах). Существует отдельным проходом, а не
|
|
* как часть постпроцесса, по одной причине: канвас матрицы создаётся с
|
|
* `alpha: false` и каждый кадр полностью перезаписывается сверху, поэтому
|
|
* любой слой, размещённый в DOM ПОЗАДИ канваса, был бы невидим — единственный
|
|
* способ дать дождю "глубину сзади" — нарисовать эту сцену первой внутри того
|
|
* же фреймбуфера, чтобы капли (которые уже блендятся поверх фона через
|
|
* SRC_ALPHA/ONE_MINUS_SRC_ALPHA) частично её перекрывали, а не полностью
|
|
* скрывали.
|
|
* Использует vsOverlay (тот же полноэкранный quad, что и постпроцесс).
|
|
* Переиспользует vsOverlay — отдельный вершинный шейдер не нужен, оба прохода
|
|
* рисуют один и тот же fullscreen-quad в clip space.
|
|
*/
|
|
export const fsBackground = `#version 300 es
|
|
precision highp float;
|
|
|
|
in vec2 v_uv;
|
|
out vec4 outColor;
|
|
|
|
uniform float u_time;
|
|
uniform vec2 u_resolution;
|
|
uniform float u_isDarkMode;
|
|
uniform float u_isVpnOn;
|
|
|
|
float hash11(float p) { return fract(sin(p * 127.1) * 43758.5453123); }
|
|
float hash21(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
|
|
|
|
// SDF прямоугольника со скруглёнными углами (для корпуса машины).
|
|
float sdRoundBox(vec2 p, vec2 b, float r) {
|
|
vec2 q = abs(p) - b + r;
|
|
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
|
}
|
|
|
|
void main() {
|
|
vec2 uv = v_uv; // 0..1, y=0 низ экрана, y=1 верх (см. vsOverlay: v_uv = a_position*0.5+0.5)
|
|
bool isLight = u_isDarkMode < 0.5;
|
|
// Акцентный цвет синхронизирован с палитрой капель/глаз: циан выкл, фиолет вкл.
|
|
vec3 accent = mix(vec3(0.0, 0.85, 1.0), vec3(0.65, 0.15, 1.0), u_isVpnOn);
|
|
// Коррекция соотношения сторон (тот же приём, что в vsMatrix/postProcessSource):
|
|
// без него все SDF-формы (контуры зданий, кузов и фары машин) искажаются
|
|
// при смене aspect ratio экрана — например, машина, нормально выглядящая
|
|
// на широком десктопе, на узком мобильном портрете превращается в
|
|
// вытянутый вертикально силуэт, потому что uv.x и uv.y покрывают разное
|
|
// физическое расстояние на пиксель. Умножаем x-компоненту локальных
|
|
// координат на aspect.x перед любым SDF/distance-вычислением — тогда
|
|
// 1 условная единица по x и y соответствует одному и тому же физическому
|
|
// расстоянию независимо от формы экрана.
|
|
vec2 aspect = vec2(u_resolution.x / max(u_resolution.y, 1.0), 1.0);
|
|
|
|
float horizon = 0.46;
|
|
vec3 skyTop = isLight ? vec3(0.85, 0.87, 0.93) : vec3(0.03, 0.035, 0.06);
|
|
vec3 skyBottom = isLight ? vec3(0.90, 0.88, 0.90) : vec3(0.05, 0.04, 0.07);
|
|
vec3 col = mix(skyBottom, skyTop, smoothstep(horizon, 1.0, uv.y));
|
|
|
|
// Светящаяся полоса на линии горизонта — приглушена, чтобы не спорить с дождём.
|
|
col += accent * exp(-abs(uv.y - horizon) * 60.0) * 0.28;
|
|
|
|
if (uv.y < horizon) {
|
|
// --- Дорога: перспектива к точке схода на линии горизонта ---
|
|
float t = clamp(1.0 - uv.y / horizon, 0.0, 1.0);
|
|
float roadHalf = mix(0.006, 0.40, pow(t, 1.6));
|
|
float distFromCenter = abs(uv.x - 0.5);
|
|
// Асфальт — ровно цвет фона (без отдельного тёмного/синтвейв оттенка),
|
|
// с мелким зернистым шумом акцентным цветом поверх — асфальтная
|
|
// "крошка", а не гладкая заливка. Шум считается в пиксельных ячейках
|
|
// экрана, а не в uv, чтобы зерно было мелким и стабильным независимо
|
|
// от масштаба перспективы дороги.
|
|
vec3 asphalt = skyBottom;
|
|
float grain = hash21(floor(uv * u_resolution)) * 2.0 - 1.0;
|
|
asphalt += accent * grain * 0.035;
|
|
|
|
if (distFromCenter < roadHalf) {
|
|
col = asphalt;
|
|
float edgeWidth = mix(0.0015, 0.012, t);
|
|
float edge = smoothstep(edgeWidth, 0.0, roadHalf - distFromCenter);
|
|
col = mix(col, accent, edge * 0.55);
|
|
|
|
// Прерывистая осевая линия, "бегущая" на зрителя.
|
|
float laneW = mix(0.0008, 0.006, t);
|
|
if (distFromCenter < laneW && fract(t * 9.0 - u_time * 0.6) < 0.5) {
|
|
col = mix(col, accent, 0.6 * smoothstep(0.0, 0.08, t));
|
|
}
|
|
}
|
|
|
|
// --- Машины: чётные слоты едут НА зрителя по левой полосе, нечётные —
|
|
// ОТ зрителя по правой полосе (эффект двустороннего движения).
|
|
for (int i = 0; i < 6; i++) {
|
|
float fi = float(i);
|
|
bool approaching = mod(fi, 2.0) < 0.5;
|
|
float speed = 0.12 + hash11(fi * 7.0) * 0.05;
|
|
float seed = hash11(fi * 13.0) * 10.0;
|
|
float tc = fract(u_time * speed + seed);
|
|
if (!approaching) tc = 1.0 - tc;
|
|
|
|
float carRoadHalf = mix(0.006, 0.40, pow(tc, 1.6));
|
|
float cx = 0.5 + (approaching ? -0.45 : 0.45) * carRoadHalf;
|
|
float cy = horizon * (1.0 - tc);
|
|
float carW = mix(0.004, 0.028, tc);
|
|
|
|
// Стилизованный силуэт вместо однородного цветного пятна: тёмный
|
|
// корпус (кузов + более узкая "кабина" сверху) с двумя отдельными
|
|
// огоньками у нижних углов — фары (белые) едущим на зрителя,
|
|
// стоп-сигналы (красные) едущим от зрителя. Даёт узнаваемый
|
|
// "машинный" силуэт даже на маленьком размере, а не просто блик.
|
|
// carWA — та же полуширина, но в изотропных координатах (см. aspect
|
|
// выше): все формы машины строятся в этих единицах, поэтому кузов
|
|
// и фары остаются одной и той же формы на любом соотношении сторон.
|
|
float fade = smoothstep(0.0, 0.06, tc);
|
|
vec2 p = uv - vec2(cx, cy);
|
|
vec2 pA = vec2(p.x * aspect.x, p.y);
|
|
float carWA = carW * aspect.x;
|
|
|
|
float bodyD = sdRoundBox(pA, vec2(carWA, carWA * 0.55 * 0.55), carWA * 0.3);
|
|
float cabinD = sdRoundBox(pA - vec2(0.0, carWA * 0.55 * 0.32), vec2(carWA * 0.6, carWA * 0.55 * 0.4), carWA * 0.22);
|
|
float carD = min(bodyD, cabinD);
|
|
|
|
vec3 bodyCol = mix(skyBottom * 0.5, accent, 0.12);
|
|
if (carD < 0.0) col = mix(col, bodyCol, fade);
|
|
|
|
// Тонкий контурный блик кузова — та же "wireframe"-стилистика, что у зданий.
|
|
float edgeLine = smoothstep(carWA * 0.12, 0.0, abs(carD)) * fade;
|
|
col = mix(col, accent, edgeLine * 0.5);
|
|
|
|
// Огоньки: два маленьких кружка у нижних углов корпуса.
|
|
vec3 lightCol = approaching ? vec3(0.95, 0.98, 1.0) : vec3(1.0, 0.15, 0.2);
|
|
float lightR = carWA * 0.22;
|
|
vec2 lightOff = vec2(carWA * 0.62, -carWA * 0.55 * 0.4);
|
|
float dl = length(pA - vec2(-lightOff.x, lightOff.y)) - lightR;
|
|
float dr = length(pA - vec2(lightOff.x, lightOff.y)) - lightR;
|
|
float lightD = min(dl, dr);
|
|
float lightMask = smoothstep(lightR * 0.4, -lightR * 0.4, lightD) * fade;
|
|
col = mix(col, lightCol, lightMask);
|
|
|
|
// Приглушённый и компактный блум — только вокруг самих огоньков,
|
|
// не вокруг всего корпуса.
|
|
float glow = exp(-max(lightD, 0.0) / (carWA * 0.35)) * fade * 0.18;
|
|
col += lightCol * glow;
|
|
}
|
|
}
|
|
|
|
outColor = vec4(col, 1.0);
|
|
}
|
|
`.trim();
|
|
|
|
/**
|
|
* Полноэкранный фрагментный шейдер второго прохода — самый плотный файл
|
|
* пайплайна. Читает готовую картинку матрицы+глитчей из postTexture
|
|
* (см. render() в matrix_worker.js) и накладывает поверх все "объективные"
|
|
* эффекты разом: CRT-дисторсию координат, хроматическую аберрацию,
|
|
* VHS-тиры/разрывы кадра, ударную волну-рябь и процедурный рендер двух
|
|
* кибер-глаз. Два прохода (матрица в текстуру, потом пост-процесс на экран)
|
|
* нужны именно потому, что многие эффекты здесь (линзы, дисторсия, CA)
|
|
* обязаны сэмплить УЖЕ готовое изображение матрицы со смещёнными
|
|
* координатами — сделать это в один проход поверх инстансированных капель
|
|
* невозможно, т.к. у каждой капли нет доступа к соседним пикселям.
|
|
*
|
|
* Самое неочевидное:
|
|
* - crt_coords(uv, bend) — классическая barrel-дисторсия (координаты
|
|
* разъезжаются от центра к краям пропорционально offset^2), имитирует
|
|
* выпуклость старого CRT-монитора; отключается на isLowQuality для мобильных.
|
|
* - applyClassicEye() — SDF-подобная функция века: createLidCurve() строит
|
|
* верхнее/нижнее веко смесью cos-дуги и степенной кривой (pow(1-|t|^p,1/p)),
|
|
* eyeSDF = max(y - upperY, lowerY - y) — расстояние до формы глаза одним
|
|
* числом (>0 снаружи века, <=0 внутри), дальше эта дистанция переиспользуется
|
|
* и для свечения века, и для ресниц (через hash-based "шипы"), и для радужки.
|
|
* - Зрачок-"солнце": внутри радужки рисуется треугольник (правый глаз) или
|
|
* ромб (левый) через ручные SDF-выражения (dTri/dRom) — геометрическая
|
|
* заглушка вместо текстуры, чтобы не тащить лишний спрайт и держать эффект
|
|
* параметрическим (масштабируется вместе с scale/pixelScale).
|
|
* - themePrimary/themeSecondary/themeBgDark/themeAccent и caruncleArea —
|
|
* захардкоженные цвета темы и "область слёзного мясца" (внутренний уголок
|
|
* глаза, caruncleArea), дающие глазам анатомическую достоверность без
|
|
* текстур; числа синхронизированы вручную с палитрой Tailwind/темой сайта
|
|
* (see themePrimary ~ violet-900, themeAccent ~ rose-600).
|
|
* - u_vpnTimer/vpnTear — при переключении VPN на 1.0 секунды экран рвётся
|
|
* горизонтальными блоками с сдвигом (аналог VHS tracking error), давая
|
|
* ощутимый визуальный отклик на THEME-сообщение из воркера.
|
|
*/
|
|
export 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_eyeClosedness;
|
|
uniform float u_isVpnOn;
|
|
uniform float u_eyeActive;
|
|
uniform vec2 u_eyePosL;
|
|
uniform vec2 u_eyePosR;
|
|
uniform vec2 u_gaze;
|
|
uniform vec3 u_shockwave;
|
|
uniform float u_isLowQuality;
|
|
uniform float u_distortion;
|
|
uniform float u_scanlines;
|
|
uniform float u_noise;
|
|
uniform float u_isDarkMode;
|
|
uniform float u_vpnTimer;
|
|
uniform float u_pixelScale;
|
|
|
|
float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
|
|
|
|
float noise(vec2 p) {
|
|
vec2 i = floor(p);
|
|
vec2 f = fract(p);
|
|
f = f*f*(3.0-2.0*f);
|
|
float a = hash(i);
|
|
float b = hash(i + vec2(1.0, 0.0));
|
|
float c = hash(i + vec2(0.0, 1.0));
|
|
float d = hash(i + vec2(1.0, 1.0));
|
|
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
}
|
|
|
|
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;
|
|
return uv * 0.5 + 0.5;
|
|
}
|
|
|
|
vec2 createLidCurve(float t, float tilt, float sideSign, float closed, float powerUpper, float powerLower, float baseHeightUpper, float baseHeightLower) {
|
|
float curveU = pow(1.0 - pow(abs(t), powerUpper), 1.0/powerUpper);
|
|
float curveL = pow(1.0 - pow(abs(t), powerLower), 1.0/powerLower);
|
|
|
|
float skewedT = t - tilt * (1.0 - t*t);
|
|
float cosCurve = cos(skewedT * 1.57079632);
|
|
|
|
float verticalTilt = skewedT * 2.5 * sideSign * (1.0 - closed);
|
|
|
|
float finalUpper = mix(cosCurve, curveU, 0.4) * baseHeightUpper + verticalTilt;
|
|
float finalLower = mix(cosCurve, curveL, 0.5) * baseHeightLower - verticalTilt;
|
|
|
|
return vec2(finalUpper, finalLower);
|
|
}
|
|
|
|
void applyClassicEye(vec2 uv, vec2 eyePosNormalized, vec2 gaze, float rawClosed, bool isVpn, bool isRightEye, inout vec3 col) {
|
|
vec2 pixelPos = uv * u_resolution;
|
|
vec2 center = eyePosNormalized * u_resolution;
|
|
vec2 p = pixelPos - center;
|
|
float sideSign = isRightEye ? 1.0 : -1.0;
|
|
|
|
// ИСПРАВЛЕННЫЙ РАСЧЕТ МАСШТАБА ДЛЯ XIAOMI
|
|
float logicalWidth = u_resolution.x;
|
|
float scale = mix(0.75, 1.1, smoothstep(320.0, 1000.0, logicalWidth));
|
|
// ----------------------------------------
|
|
|
|
bool isLight = u_isDarkMode < 0.5;
|
|
|
|
vec3 themePrimary = vec3(0.3, 0.0, 0.6);
|
|
vec3 themeSecondary = vec3(0.0, 0.4, 0.5);
|
|
vec3 themeBgDark = vec3(0.043, 0.051, 0.090);
|
|
vec3 themeBgLight = vec3(0.973, 0.980, 0.988);
|
|
vec3 themeAccent = vec3(1.0, 0.0, 0.2);
|
|
|
|
vec3 colorPurple = isVpn ? vec3(0.8, 0.0, 1.0) : themePrimary;
|
|
vec3 colorCyan = isVpn ? vec3(1.0, 0.0, 0.0) : themeSecondary;
|
|
vec3 eyeScleraColor = isLight ? themeBgLight : vec3(0.02);
|
|
vec3 eyeLinerColor = isLight ? themePrimary * 0.3 : vec3(0.005);
|
|
|
|
float baseWidth = 72.0 * scale;
|
|
float baseHeightUpper = 32.0 * scale;
|
|
float baseHeightLower = 18.0 * scale;
|
|
|
|
float xNorm = clamp(p.x / baseWidth, -1.0, 1.0);
|
|
float tilt = 0.15 * sideSign;
|
|
|
|
float bounce = sin(rawClosed * 3.1415 * 2.5) * 0.05 * sin(rawClosed * 3.1415);
|
|
float closed = clamp(rawClosed - bounce, 0.0, 1.0);
|
|
|
|
vec2 curves = createLidCurve(xNorm, tilt, sideSign, closed, 2.0, 1.8, baseHeightUpper, baseHeightLower);
|
|
|
|
float meetY = mix(curves.x, -curves.y, 0.75);
|
|
|
|
float currentUpperY = mix(curves.x, meetY, closed);
|
|
float currentLowerY = mix(-curves.y, meetY, closed);
|
|
|
|
float eyeSDF = max(p.y - currentUpperY, currentLowerY - p.y);
|
|
float sdfScale = scale * mix(1.0, 0.6, closed);
|
|
|
|
float horizontalFade = 1.0 - smoothstep(0.9, 1.0, abs(xNorm));
|
|
if (horizontalFade == 0.0 || (p.x / baseWidth) < -1.0 || (p.x / baseWidth) > 1.0) return;
|
|
|
|
if (eyeSDF > 0.0) {
|
|
float glow = exp(-eyeSDF / (5.0 * sdfScale)) * horizontalFade;
|
|
if (isLight) {
|
|
col = mix(col, colorPurple, glow * (isVpn ? 0.6 : 0.25));
|
|
} else {
|
|
col += colorPurple * glow * (isVpn ? 0.7 : 0.4);
|
|
}
|
|
|
|
float linerMask = (1.0 - smoothstep(0.0, 2.5 * sdfScale, eyeSDF)) * horizontalFade;
|
|
col = mix(col, eyeLinerColor, linerMask);
|
|
|
|
float edge = (1.0 - smoothstep(0.0, 1.2 * sdfScale, eyeSDF)) * horizontalFade;
|
|
vec3 edgeColor = isLight ? themeSecondary : themeAccent;
|
|
col = mix(col, edgeColor, edge * 0.9);
|
|
|
|
float lashFreq = 22.0;
|
|
float ySign = sign(p.y - meetY);
|
|
float lashFract = fract(xNorm * lashFreq + ySign * 0.2);
|
|
float spikeShape = 1.0 - smoothstep(0.0, 0.3, abs(lashFract - 0.5));
|
|
|
|
float maxSpikeLen = (8.0 + hash(vec2(floor(xNorm * lashFreq), ySign)) * 12.0) * scale * horizontalFade;
|
|
if (isVpn) maxSpikeLen *= 1.3;
|
|
|
|
float lashMask = 1.0 - smoothstep(0.0, 1.5 * sdfScale, eyeSDF - maxSpikeLen * spikeShape);
|
|
|
|
if (lashMask > 0.0) {
|
|
vec3 lashBaseCol = isLight ? themePrimary * 0.8 : vec3(0.01);
|
|
vec3 lashTipCol = isVpn ? themeAccent : themeSecondary;
|
|
float tipGradient = smoothstep(0.0, maxSpikeLen, eyeSDF);
|
|
vec3 finalLashCol = mix(lashBaseCol, lashTipCol, tipGradient * 1.5);
|
|
col = mix(col, finalLashCol, lashMask);
|
|
}
|
|
return;
|
|
}
|
|
|
|
vec3 innerCol = eyeScleraColor;
|
|
|
|
float caruncleArea = isRightEye ? (1.0 - smoothstep(-1.0, -0.7, xNorm)) : smoothstep(0.7, 1.0, xNorm);
|
|
vec3 caruncleCol = isLight ? vec3(0.8, 0.4, 0.4) : vec3(0.15, 0.02, 0.02);
|
|
innerCol = mix(innerCol, caruncleCol, caruncleArea * smoothstep(0.0, 10.0*scale, curves.x - abs(p.y)) * 0.4);
|
|
|
|
float gridSpeed = isVpn ? u_time * 12.0 : u_time * 3.0;
|
|
float currentFullHeight = p.y > 0.0 ? abs(currentUpperY) : abs(currentLowerY);
|
|
float perspY = p.y / max(currentFullHeight, 0.1 * scale);
|
|
|
|
float gridY = fract((perspY * 10.0 * scale - gridSpeed) / (4.0 * scale));
|
|
float gridLines = smoothstep(0.85, 1.0, gridY);
|
|
float gridX = fract((p.x - (isVpn ? u_time * 15.0 : 0.0)) / (8.0 * scale));
|
|
float gridLinesX = smoothstep(0.9, 1.0, gridX);
|
|
|
|
float gridIntensity = gridLines + gridLinesX;
|
|
vec3 gridActiveColor = isLight ? themePrimary * 0.6 : colorPurple * 0.5;
|
|
float gridMix = isLight ? (gridIntensity * 0.5) : (gridIntensity * (0.2 + 0.8 * abs(perspY)));
|
|
innerCol = mix(innerCol, gridActiveColor, gridMix * (1.0 - caruncleArea * 0.8));
|
|
|
|
vec2 gazeOffset = gaze * vec2(15.0, 6.0) * scale;
|
|
vec2 pupilP = p - gazeOffset;
|
|
float r = length(pupilP);
|
|
float irisRadius = 20.0 * scale;
|
|
|
|
float irisMask = 1.0 - smoothstep(irisRadius - 1.5 * scale, irisRadius, r);
|
|
|
|
if (irisMask > 0.0) {
|
|
innerCol = mix(innerCol, isLight ? themeBgLight : vec3(0.01), irisMask);
|
|
float stripeDensity = isVpn ? 1.8 : 3.2;
|
|
float sunStripes = step(0.4, fract((pupilP.y - u_time * 4.0) / (stripeDensity * scale)));
|
|
|
|
vec3 colorBottom = isVpn ? vec3(1.0, 0.0, 0.4) : vec3(0.0, 0.7, 1.0);
|
|
vec3 colorTop = isVpn ? vec3(0.8, 0.0, 1.0) : vec3(0.6, 0.0, 1.0);
|
|
|
|
vec3 sunGradient = mix(colorBottom, colorTop, smoothstep(-irisRadius, irisRadius, pupilP.y));
|
|
float sunGlow = 1.0 - smoothstep(0.0, irisRadius, r);
|
|
|
|
vec3 sunBaseColor;
|
|
if (isLight) {
|
|
sunBaseColor = sunGradient * (sunStripes * 0.5 + 0.5);
|
|
} else {
|
|
sunBaseColor = sunGradient * (sunStripes * 0.7 + 0.3) * sunGlow * 2.0;
|
|
}
|
|
innerCol = mix(innerCol, sunBaseColor, irisMask);
|
|
|
|
float rotAngle = u_time * 1.5 * (isRightEye ? 1.0 : -1.0);
|
|
float s = sin(rotAngle), c = cos(rotAngle);
|
|
vec2 rotPupil = vec2(pupilP.x * c - pupilP.y * s, pupilP.x * s + pupilP.y * c);
|
|
float pupilRadius = isVpn ? 8.0 * scale : 5.0 * scale;
|
|
|
|
if (isRightEye) {
|
|
float dTri = max(abs(rotPupil.x) * 0.866025 + rotPupil.y * 0.5, -rotPupil.y) - pupilRadius;
|
|
float triMask = 1.0 - smoothstep(0.0, 1.5*scale, dTri);
|
|
innerCol = mix(innerCol, isLight ? themeBgLight : vec3(0.0), triMask);
|
|
vec3 triNeon = isLight ? themePrimary : colorBottom * 2.5;
|
|
innerCol = mix(innerCol, triNeon, (1.0 - smoothstep(0.0, 1.5*scale, abs(dTri))) * triMask);
|
|
} else {
|
|
float dRom = abs(rotPupil.x) + abs(rotPupil.y) - pupilRadius * 1.2;
|
|
float romMask = 1.0 - smoothstep(0.0, 1.5*scale, dRom);
|
|
innerCol = mix(innerCol, isLight ? themeBgLight : vec3(0.0), romMask);
|
|
vec3 romNeon = isLight ? themePrimary : colorTop * 2.5;
|
|
innerCol = mix(innerCol, romNeon, (1.0 - smoothstep(0.0, 1.5*scale, abs(dRom))) * romMask);
|
|
}
|
|
float specH = (1.0 - smoothstep(0.0, 1.5*scale, abs(pupilP.y - 6.0*scale))) * (1.0 - smoothstep(0.0, 9.0*scale, abs(pupilP.x + 4.5*scale)));
|
|
innerCol = mix(innerCol, vec3(1.0), specH * irisMask * (isLight ? 0.8 : 1.0));
|
|
}
|
|
|
|
if (isVpn) {
|
|
float scanY = sin(u_time * 4.0) * baseHeightUpper;
|
|
float scanMask = exp(-abs(p.y - scanY) / (1.2 * scale));
|
|
innerCol = mix(innerCol, colorCyan, scanMask * (isLight ? 0.4 : 0.6));
|
|
}
|
|
|
|
float shadowOcclusion = smoothstep(0.0, 16.0 * scale, currentUpperY - p.y);
|
|
if (p.y > -8.0*scale) {
|
|
if (isLight) {
|
|
innerCol = mix(innerCol, themePrimary * 0.2, (1.0 - shadowOcclusion) * 0.15);
|
|
} else {
|
|
innerCol *= (0.2 + 0.8 * shadowOcclusion);
|
|
}
|
|
}
|
|
|
|
col = innerCol;
|
|
}
|
|
|
|
void main() {
|
|
vec2 baseUv = (u_isLowQuality > 0.5) ? v_uv : crt_coords(v_uv, u_distortion);
|
|
bool isLight = u_isDarkMode < 0.5;
|
|
|
|
// ИСПРАВЛЕННЫЙ РАСЧЕТ МАСШТАБА ДЛЯ XIAOMI
|
|
float pScale = max(u_pixelScale, 0.1);
|
|
float logicalWidth = u_resolution.x;
|
|
float scale = mix(0.75, 1.1, smoothstep(320.0, 1000.0, logicalWidth)) * pScale;
|
|
// ----------------------------------------
|
|
|
|
vec2 lensOffset = vec2(0.0);
|
|
if (u_eyeActive > 0.5 && u_isLowQuality < 0.5) {
|
|
vec2 eL = u_eyePosL / u_resolution; eL.y = 1.0 - eL.y;
|
|
vec2 eR = u_eyePosR / u_resolution; eR.y = 1.0 - eR.y;
|
|
|
|
vec2 pL = baseUv * u_resolution - eL * u_resolution;
|
|
vec2 pR = baseUv * u_resolution - eR * u_resolution;
|
|
|
|
float rL = length(pL);
|
|
float rR = length(pR);
|
|
float lensRad = 45.0 * scale;
|
|
|
|
float refrForce = (u_isVpnOn > 0.5) ? 0.15 : 0.04;
|
|
|
|
if (rL < lensRad) {
|
|
float profile = 1.0 - smoothstep(0.0, lensRad, rL);
|
|
if (u_isVpnOn > 0.5) profile = floor(profile * 4.0) / 4.0;
|
|
lensOffset -= (pL / u_resolution) * refrForce * profile;
|
|
}
|
|
if (rR < lensRad) {
|
|
float profile = 1.0 - smoothstep(0.0, lensRad, rR);
|
|
if (u_isVpnOn > 0.5) profile = floor(profile * 4.0) / 4.0;
|
|
lensOffset -= (pR / u_resolution) * refrForce * profile;
|
|
}
|
|
}
|
|
|
|
vec2 glitchOffset = vec2(0.0);
|
|
|
|
vec2 grid = floor(baseUv * u_resolution / vec2(80.0, 40.0));
|
|
float glitchVal = hash(grid + floor(u_time * 12.0));
|
|
if (glitchVal > 0.998) glitchOffset.x += (hash(grid) - 0.5) * 0.05;
|
|
|
|
float tearHash = hash(vec2(floor(u_time * 8.0), 13.37));
|
|
float isTearing = step(0.97, tearHash);
|
|
float caOffset = 0.0;
|
|
|
|
if (isTearing > 0.5) {
|
|
float tearY = hash(vec2(floor(u_time * 8.0), 24.68));
|
|
float tearBand = 1.0 - smoothstep(0.0, 0.04, abs(baseUv.y - tearY));
|
|
glitchOffset.x += tearBand * (hash(vec2(baseUv.y * 100.0, u_time)) - 0.5) * 0.2;
|
|
caOffset = tearBand * 0.05;
|
|
}
|
|
|
|
float vpnTear = 0.0;
|
|
if (u_vpnTimer > 0.0) {
|
|
float intensity = smoothstep(0.0, 1.0, u_vpnTimer);
|
|
float tearBlock = floor(baseUv.y * 20.0 + u_time * 15.0);
|
|
float blockHash = hash(vec2(tearBlock, floor(u_time * 10.0)));
|
|
if (blockHash > 0.4) {
|
|
float shift = (hash(vec2(tearBlock, 2.0)) - 0.5) * 0.08 * intensity;
|
|
glitchOffset.x += shift;
|
|
vpnTear = intensity;
|
|
}
|
|
}
|
|
|
|
float wave = 0.0;
|
|
vec2 swDir = vec2(0.0);
|
|
|
|
if (u_shockwave.z > 0.01) {
|
|
vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);
|
|
vec2 centerDiff = (baseUv + glitchOffset) * aspect - u_shockwave.xy * aspect;
|
|
float baseDist = length(centerDiff);
|
|
|
|
swDir = normalize(centerDiff);
|
|
|
|
float angle = atan(swDir.y, swDir.x);
|
|
float digitalNoise = hash(vec2(floor(angle * 12.0), floor(u_time * 8.0)));
|
|
float spike = step(0.8, digitalNoise) * (digitalNoise - 0.8) * 0.4;
|
|
float wobble = noise(vec2(angle * 6.0, u_time * 5.0)) * 0.04;
|
|
float distortedDist = baseDist + spike - wobble;
|
|
|
|
float ringWidth = 0.015;
|
|
wave = (1.0 - smoothstep(u_shockwave.z, u_shockwave.z + ringWidth, distortedDist)) * smoothstep(u_shockwave.z - ringWidth, u_shockwave.z, distortedDist);
|
|
|
|
glitchOffset -= swDir * wave * 0.025;
|
|
}
|
|
|
|
vec2 bgUv = baseUv + lensOffset + glitchOffset;
|
|
vec2 eyeUv = baseUv + glitchOffset;
|
|
|
|
vec3 color = texture(u_mainTex, bgUv).rgb;
|
|
|
|
vec3 themePrimary = vec3(0.3, 0.0, 0.6);
|
|
vec3 themeSecondary = vec3(0.0, 0.4, 0.5);
|
|
vec3 themeAccent = vec3(1.0, 0.0, 0.2);
|
|
|
|
if (vpnTear > 0.0) {
|
|
color.r = texture(u_mainTex, bgUv + vec2(0.015 * vpnTear, 0.0)).r;
|
|
color.g = texture(u_mainTex, bgUv).g;
|
|
color.b = texture(u_mainTex, bgUv - vec2(0.015 * vpnTear, 0.0)).b;
|
|
|
|
float colorBand = fract(baseUv.y * 3.0 - u_time * 10.0);
|
|
vec3 glitchColor = mix(themeSecondary, themeAccent, step(0.33, colorBand));
|
|
glitchColor = mix(glitchColor, themePrimary, step(0.66, colorBand));
|
|
|
|
color = mix(color, glitchColor, vpnTear * (isLight ? 0.25 : 0.35));
|
|
color += glitchColor * vpnTear * 0.15;
|
|
}
|
|
|
|
if (caOffset > 0.0) {
|
|
color.r = texture(u_mainTex, bgUv + vec2(caOffset, 0.0)).r;
|
|
color.b = texture(u_mainTex, bgUv - vec2(caOffset, 0.0)).b;
|
|
color += (isLight ? themePrimary : vec3(0.7, 0.2, 1.0)) * caOffset * 10.0;
|
|
}
|
|
|
|
if (wave > 0.0) {
|
|
color.r = texture(u_mainTex, bgUv + swDir * wave * 0.005).r;
|
|
color.b = texture(u_mainTex, bgUv - swDir * wave * 0.005).b;
|
|
}
|
|
|
|
if (u_eyeActive > 0.5) {
|
|
vec2 eyeL = u_eyePosL / u_resolution;
|
|
vec2 eyeR = u_eyePosR / u_resolution;
|
|
eyeL.y = 1.0 - eyeL.y;
|
|
eyeR.y = 1.0 - eyeR.y;
|
|
|
|
applyClassicEye(eyeUv, eyeL, u_gaze, u_eyeClosedness, u_isVpnOn > 0.5, false, color);
|
|
applyClassicEye(eyeUv, eyeR, u_gaze, u_eyeClosedness, u_isVpnOn > 0.5, true, color);
|
|
}
|
|
|
|
if (wave > 0.0) {
|
|
vec3 shockwaveColor = (u_isVpnOn > 0.5) ? vec3(1.0, 0.0, 0.3) : vec3(0.4, 0.0, 1.0);
|
|
color += wave * 0.4 * shockwaveColor;
|
|
}
|
|
|
|
float scanline = sin(v_uv.y * u_resolution.y * 2.2) * 0.02;
|
|
color -= isLight ? scanline * 0.5 : scanline;
|
|
float vignette = clamp(pow(16.0 * v_uv.x * v_uv.y * (1.0 - v_uv.x) * (1.0 - v_uv.y), 0.25), 0.0, 1.0);
|
|
outColor = vec4(color * mix(0.9, 1.0, vignette), 1.0);
|
|
}
|
|
`.trim();
|