2105 lines
131 KiB
JavaScript
2105 lines
131 KiB
JavaScript
/* Главная git.zapret.moe: заставка-презентация, музыка к ней и «Пульс разработки».
|
||
Всё здесь — необязательное улучшение: без JS страница полностью работает. */
|
||
(function () {
|
||
'use strict';
|
||
|
||
var root = document.documentElement;
|
||
var DAY = 86400;
|
||
var MONTHS = ['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'];
|
||
var COLORS = { gui: '#4fd1ff', zsg: '#9b7bff', kvn: '#3b82ff', magisk: '#ff6bb3', vpn: '#37e39a', other: '#7b89b3', all: '#4fd1ff' };
|
||
|
||
function fmt(n) { return Math.round(n).toLocaleString('ru-RU') }
|
||
function short(n) {
|
||
if (n >= 1e6) return (n / 1e6).toLocaleString('ru-RU', { maximumFractionDigits: 1 }) + ' млн';
|
||
if (n >= 1e4) return Math.round(n / 1e3).toLocaleString('ru-RU') + ' тыс.';
|
||
return fmt(n);
|
||
}
|
||
function plural(n, one, few, many) {
|
||
var m10 = n % 10, m100 = n % 100;
|
||
if (m10 === 1 && m100 !== 11) return one;
|
||
if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return few;
|
||
return many;
|
||
}
|
||
function ease(t) { return 1 - Math.pow(1 - t, 3) }
|
||
function countUp(el, to, ms) {
|
||
var t0 = performance.now();
|
||
(function step(now) {
|
||
var k = Math.min(1, (now - t0) / ms);
|
||
el.textContent = fmt(to * (1 - Math.pow(2, -10 * k)) / (1 - Math.pow(2, -10)));
|
||
if (k < 1) requestAnimationFrame(step); else el.textContent = fmt(to);
|
||
})(t0);
|
||
}
|
||
function weekLabel(ts) {
|
||
var a = new Date(ts * 1000), b = new Date((ts + 6 * DAY) * 1000);
|
||
return a.getUTCDate() + (a.getUTCMonth() === b.getUTCMonth() ? '' : ' ' + MONTHS[a.getUTCMonth()]) +
|
||
'–' + b.getUTCDate() + ' ' + MONTHS[b.getUTCMonth()];
|
||
}
|
||
|
||
/* ── Данные: готовый JSON с сервера, запасной вариант — heatmap API ───── */
|
||
var pulse = document.getElementById('zpm-pulse');
|
||
var statsPromise = null;
|
||
function loadStats() {
|
||
if (statsPromise) return statsPromise;
|
||
if (!pulse || !window.fetch) return (statsPromise = Promise.reject());
|
||
var bust = Math.floor(Date.now() / 1800000);
|
||
statsPromise = fetch(pulse.getAttribute('data-stats') + '?h=' + bust)
|
||
.then(function (r) { if (!r.ok) throw 0; return r.json() })
|
||
.then(function (d) { d.source = 'commits'; return d })
|
||
.catch(function () {
|
||
return fetch(pulse.getAttribute('data-heatmap')).then(function (r) { return r.json() }).then(function (hm) {
|
||
var now = Date.now() / 1000, monday = now - (now - 4 * DAY) % (7 * DAY), week0 = monday - 51 * 7 * DAY;
|
||
var s = []; for (var i = 0; i < 52; i++) s.push(0);
|
||
var t = { commits_year: 0, commits_7d: 0 };
|
||
hm.forEach(function (p) {
|
||
if (p.timestamp >= week0) s[Math.min(51, Math.floor((p.timestamp - week0) / (7 * DAY)))] += p.contributions;
|
||
if (now - p.timestamp < 365 * DAY) t.commits_year += p.contributions;
|
||
if (now - p.timestamp < 7 * DAY) t.commits_7d += p.contributions;
|
||
});
|
||
return { source: 'actions', week0: week0, order: ['all'], labels: { all: 'Все проекты' }, series: { all: s }, totals: t };
|
||
});
|
||
});
|
||
return statsPromise;
|
||
}
|
||
|
||
/* ── Интерактивный график ─────────────────────────────────────────────── */
|
||
function Chart(box, data) {
|
||
var svg = box.querySelector('svg'), tip = box.querySelector('.zp-tip');
|
||
var keys = data.order.filter(function (k) { return data.series[k] && data.series[k].some(Boolean) });
|
||
var N = data.series[keys[0]].length;
|
||
var on = {}; keys.forEach(function (k) { on[k] = true });
|
||
var cur = {}, tgt = {};
|
||
keys.forEach(function (k) { cur[k] = data.series[k].map(function () { return 0 }) });
|
||
var wCur = 52, wTgt = 52, yCur = 1, reveal = 0, hover = -1, raf = 0;
|
||
|
||
function targets() {
|
||
keys.forEach(function (k) { tgt[k] = data.series[k].map(function (v) { return on[k] ? v : 0 }) });
|
||
}
|
||
function totalsOf(src, i) { var s = 0; keys.forEach(function (k) { s += src[k][i] }); return s }
|
||
function niceMax(v) {
|
||
if (v <= 0) return 10;
|
||
var p = Math.pow(10, Math.floor(Math.log10(v))), m = v / p;
|
||
return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 2.5 ? 2.5 : m <= 5 ? 5 : 10) * p;
|
||
}
|
||
function yTarget(w) {
|
||
var m = 0;
|
||
for (var i = Math.max(0, N - Math.ceil(w)); i < N; i++) m = Math.max(m, totalsOf(tgt, i));
|
||
return niceMax(m * 1.08);
|
||
}
|
||
|
||
function draw() {
|
||
var W = box.clientWidth, H = box.clientHeight;
|
||
if (!W || !H) return;
|
||
var pl = 44, pr = 14, pt = 22, pb = 30, pw = W - pl - pr, ph = H - pt - pb;
|
||
var s0 = N - wCur;
|
||
var x = function (i) { return pl + (i - s0) / (wCur - 1) * pw };
|
||
var y = function (v) { return pt + ph - v / yCur * ph };
|
||
var out = '<defs><clipPath id="zp-clip"><rect x="' + pl + '" y="0" width="' + Math.max(0, pw * reveal).toFixed(1) + '" height="' + H + '"/></clipPath>';
|
||
keys.forEach(function (k) {
|
||
out += '<linearGradient id="zp-g-' + k + '" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="' + COLORS[k] + '" stop-opacity=".85"/><stop offset="1" stop-color="' + COLORS[k] + '" stop-opacity=".25"/></linearGradient>';
|
||
});
|
||
out += '</defs>';
|
||
// сетка и подписи
|
||
for (var g = 0; g <= 4; g++) {
|
||
var gv = yCur * g / 4, gy = y(gv).toFixed(1);
|
||
out += '<line class="zp-gl" x1="' + pl + '" x2="' + (W - pr) + '" y1="' + gy + '" y2="' + gy + '"/>';
|
||
out += '<text class="zp-yl" x="' + (pl - 8) + '" y="' + (+gy + 4) + '">' + short(gv) + '</text>';
|
||
}
|
||
var lastX = -99;
|
||
for (var i = Math.max(0, Math.floor(s0)); i < N; i++) {
|
||
var t = data.week0 + i * 7 * DAY, d = new Date(t * 1000), prev = new Date((t - 7 * DAY) * 1000);
|
||
if (d.getUTCMonth() !== prev.getUTCMonth()) {
|
||
var mx = x(i);
|
||
if (mx >= pl && mx - lastX > 40) { out += '<text class="zp-xl" x="' + mx.toFixed(1) + '" y="' + (H - 8) + '">' + MONTHS[d.getUTCMonth()] + '</text>'; lastX = mx }
|
||
}
|
||
}
|
||
// слои
|
||
out += '<g clip-path="url(#zp-clip)">';
|
||
var base = []; for (i = 0; i < N; i++) base.push(0);
|
||
var i0 = Math.max(0, Math.floor(s0) - 1);
|
||
keys.forEach(function (k) {
|
||
var top = [], j;
|
||
for (j = 0; j < N; j++) top.push(base[j] + cur[k][j]);
|
||
var up = '', down = '';
|
||
for (j = i0; j < N; j++) {
|
||
var px = x(j).toFixed(1), py = y(top[j]).toFixed(1);
|
||
if (j === i0) up = 'M' + px + ' ' + py;
|
||
else { var mx2 = ((x(j - 1) + x(j)) / 2).toFixed(1); up += ' C' + mx2 + ' ' + y(top[j - 1]).toFixed(1) + ' ' + mx2 + ' ' + py + ' ' + px + ' ' + py }
|
||
}
|
||
for (j = N - 1; j >= i0; j--) {
|
||
var bx = x(j).toFixed(1), by = y(base[j]).toFixed(1);
|
||
if (j === N - 1) down = ' L' + bx + ' ' + by;
|
||
else { var mx3 = ((x(j + 1) + x(j)) / 2).toFixed(1); down += ' C' + mx3 + ' ' + y(base[j + 1]).toFixed(1) + ' ' + mx3 + ' ' + by + ' ' + bx + ' ' + by }
|
||
}
|
||
out += '<path class="zp-ar" fill="url(#zp-g-' + k + ')" d="' + up + down + ' Z"/>';
|
||
out += '<path class="zp-ln" stroke="' + COLORS[k] + '" d="' + up + '"/>';
|
||
base = top;
|
||
});
|
||
out += '</g>';
|
||
// рекордная неделя
|
||
var best = -1, bestV = 0;
|
||
for (i = Math.max(0, Math.ceil(s0)); i < N; i++) { var tv = totalsOf(tgt, i); if (tv > bestV) { bestV = tv; best = i } }
|
||
if (best >= 0 && reveal > 0.98 && hover < 0) {
|
||
var bx2 = x(best), by2 = y(base[best]);
|
||
out += '<g class="zp-best"><circle cx="' + bx2.toFixed(1) + '" cy="' + by2.toFixed(1) + '" r="5"/><text x="' + Math.min(W - pr - 50, Math.max(pl + 50, bx2)).toFixed(1) + '" y="' + Math.max(14, by2 - 12).toFixed(1) + '">рекорд · ' + fmt(bestV) + '</text></g>';
|
||
}
|
||
// курсор
|
||
if (hover >= 0) {
|
||
var hx = x(hover).toFixed(1);
|
||
out += '<line class="zp-cur" x1="' + hx + '" x2="' + hx + '" y1="' + pt + '" y2="' + (pt + ph) + '"/>';
|
||
var acc = 0;
|
||
keys.forEach(function (k) { if (!on[k]) return; acc += cur[k][hover]; out += '<circle class="zp-dot" cx="' + hx + '" cy="' + y(acc).toFixed(1) + '" r="4" stroke="' + COLORS[k] + '"/>' });
|
||
}
|
||
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
|
||
svg.innerHTML = out;
|
||
svg.__x = x; svg.__s0 = s0; svg.__pl = pl; svg.__pw = pw;
|
||
}
|
||
|
||
function animate(ms, withReveal) {
|
||
cancelAnimationFrame(raf);
|
||
var from = {}, w0 = wCur, y0 = yCur, y1 = yTarget(wTgt), r0 = reveal, t0 = performance.now();
|
||
keys.forEach(function (k) { from[k] = cur[k].slice() });
|
||
(function step(now) {
|
||
var k = Math.min(1, (now - t0) / ms), e = ease(k);
|
||
keys.forEach(function (key) { for (var i = 0; i < N; i++) cur[key][i] = from[key][i] + (tgt[key][i] - from[key][i]) * e });
|
||
wCur = w0 + (wTgt - w0) * e; yCur = y0 + (y1 - y0) * e;
|
||
if (withReveal) reveal = r0 + (1 - r0) * ease(Math.min(1, (now - t0) / (ms * 0.9)));
|
||
draw();
|
||
if (k < 1) raf = requestAnimationFrame(step);
|
||
})(t0);
|
||
}
|
||
|
||
// подсказка
|
||
function showTip(ev) {
|
||
if (!svg.__x) return;
|
||
var r = svg.getBoundingClientRect(), px = ev.clientX - r.left;
|
||
var i = Math.round(svg.__s0 + (px - svg.__pl) / svg.__pw * (wCur - 1));
|
||
i = Math.max(Math.ceil(svg.__s0), Math.min(N - 1, i));
|
||
if (i !== hover) { hover = i; draw() }
|
||
var rows = keys.filter(function (k) { return on[k] && data.series[k][i] > 0 })
|
||
.sort(function (a, b) { return data.series[b][i] - data.series[a][i] });
|
||
var tot = 0; rows.forEach(function (k) { tot += data.series[k][i] });
|
||
var word = data.source === 'commits' ? plural(tot, 'коммит', 'коммита', 'коммитов') : plural(tot, 'действие', 'действия', 'действий');
|
||
tip.innerHTML = '<h4>' + weekLabel(data.week0 + i * 7 * DAY) + '</h4><p class="zp-tt">' + fmt(tot) + ' <small>' + word + '</small></p>' +
|
||
(rows.length > 1 ? '<ul>' + rows.map(function (k) { return '<li><span><i style="--c:' + COLORS[k] + '"></i>' + data.labels[k] + '</span><b>' + fmt(data.series[k][i]) + '</b></li>' }).join('') + '</ul>' : '');
|
||
tip.hidden = false;
|
||
var tx = svg.__x(i), tw = tip.offsetWidth;
|
||
tip.style.left = Math.max(tw / 2 + 4, Math.min(box.clientWidth - tw / 2 - 4, tx)) + 'px';
|
||
}
|
||
function hideTip() { hover = -1; tip.hidden = true; draw() }
|
||
svg.addEventListener('pointermove', showTip);
|
||
svg.addEventListener('pointerdown', showTip);
|
||
svg.addEventListener('pointerleave', hideTip);
|
||
|
||
// легенда
|
||
var legend = document.getElementById('zp-legend');
|
||
if (keys.length > 1) keys.forEach(function (k) {
|
||
var b = document.createElement('button');
|
||
var sum = data.series[k].reduce(function (a, v) { return a + v }, 0);
|
||
b.type = 'button'; b.className = 'zp-lg'; b.style.setProperty('--c', COLORS[k]);
|
||
b.setAttribute('aria-pressed', 'true');
|
||
b.innerHTML = '<i></i>' + data.labels[k] + ' <small>' + short(sum) + '</small>';
|
||
b.addEventListener('click', function () {
|
||
var active = keys.filter(function (q) { return on[q] });
|
||
if (on[k] && active.length === 1) { keys.forEach(function (q) { on[q] = true }) } // последний — включить всё
|
||
else on[k] = !on[k];
|
||
legend.querySelectorAll('.zp-lg').forEach(function (el, idx) { el.setAttribute('aria-pressed', on[keys[idx]] ? 'true' : 'false') });
|
||
targets(); animate(650);
|
||
});
|
||
legend.appendChild(b);
|
||
});
|
||
|
||
// период
|
||
pulse.querySelectorAll('.zp-range button').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
pulse.querySelectorAll('.zp-range button').forEach(function (o) { o.setAttribute('aria-pressed', o === b ? 'true' : 'false') });
|
||
wTgt = +b.getAttribute('data-w'); animate(750);
|
||
});
|
||
});
|
||
|
||
targets();
|
||
yCur = yTarget(52);
|
||
if (window.ResizeObserver) new ResizeObserver(function () { draw() }).observe(box);
|
||
this.play = function () { animate(1400, true) };
|
||
draw();
|
||
}
|
||
|
||
if (pulse) loadStats().then(function (data) {
|
||
var totals = data.totals || {};
|
||
pulse.querySelectorAll('dd[data-k]').forEach(function (el) {
|
||
if (totals[el.getAttribute('data-k')] == null) el.closest('.zp-kpi').hidden = true;
|
||
});
|
||
if (data.source !== 'commits') {
|
||
var main = pulse.querySelector('.zp-kpi-main dt'); if (main) main.textContent = 'действий в git за год';
|
||
}
|
||
pulse.hidden = false;
|
||
var chart = new Chart(document.getElementById('zp-chart'), data);
|
||
var started = false;
|
||
function go() {
|
||
if (started) return; started = true;
|
||
pulse.querySelectorAll('[data-k]').forEach(function (el) {
|
||
var v = totals[el.getAttribute('data-k')]; if (v != null) countUp(el, v, 1600);
|
||
});
|
||
chart.play();
|
||
}
|
||
if (window.IntersectionObserver) {
|
||
var io = new IntersectionObserver(function (es) { if (es[0].isIntersecting) { io.disconnect(); go() } }, { threshold: 0.25 });
|
||
io.observe(pulse);
|
||
} else go();
|
||
}).catch(function () {});
|
||
|
||
/* общие функции для заставки (intro.js) */
|
||
window.zpm = { loadStats: loadStats, fmt: fmt, short: short, plural: plural, countUp: countUp };
|
||
|
||
/* ── Дуэль на главной: персонажи выплывают с боков ─────────────────── */
|
||
var duel = document.getElementById('zpm-duel');
|
||
if (duel && window.IntersectionObserver && !(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches)) {
|
||
duel.classList.add('zd-js');
|
||
var dio = new IntersectionObserver(function (es) {
|
||
if (es[0].isIntersecting) { duel.classList.add('zd-on'); duel._onAt = performance.now(); dio.disconnect() }
|
||
}, { threshold: 0.2 });
|
||
dio.observe(duel);
|
||
}
|
||
|
||
/* ── Драка в дуэли: хореография на таймлайне ─────────────────────────
|
||
Бойцы в полный рост стоят лицом друг к другу (слои: волосы, основа, две руки
|
||
с осью в плече). Цикл 7,2 с: РКН-тян бросается и полосует когтями — Запрет-тян
|
||
уходит назад (промах, следы когтей); колющий удар — блок рукой (щит, искры);
|
||
контратака кулаком — вспышка, ударная волна, РКН-тян отбрасывает; ярость.
|
||
Тело двигают свойства translate/rotate/scale (Web Animations), руки — transform:
|
||
они складываются с CSS-покачиванием стойки. Точки эффектов берутся с реальных
|
||
прямоугольников кулака и когтей в момент удара. */
|
||
(function () {
|
||
if (!duel || !duel.animate || !window.IntersectionObserver) return;
|
||
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||
var zt = duel.querySelector('.zf-zt'), rk = duel.querySelector('.zf-rkn');
|
||
if (!zt || !rk) return;
|
||
var ztGuard = zt.querySelector('.zf-arm0'), ztPunch = zt.querySelector('.zf-arm1');
|
||
var rkClaw = rk.querySelector('.zf-arm0'), rkBack = rk.querySelector('.zf-arm1');
|
||
var vs = duel.querySelector('.zd-vs span');
|
||
duel.classList.add('zd-fight');
|
||
|
||
var cv = document.createElement('canvas');
|
||
cv.className = 'zd-fx'; cv.setAttribute('aria-hidden', 'true');
|
||
duel.appendChild(cv);
|
||
var g = cv.getContext('2d'), W = 0, H = 0, dpr = 1, fx = [], raf = 0;
|
||
function size() {
|
||
var r = duel.getBoundingClientRect();
|
||
dpr = Math.min(2, window.devicePixelRatio || 1); W = r.width; H = r.height;
|
||
cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr);
|
||
}
|
||
addEventListener('resize', size);
|
||
|
||
// точка на элементе (доли его прямоугольника) в координатах дуэли
|
||
function at(el, fx_, fy) {
|
||
var d = duel.getBoundingClientRect(), r = el.getBoundingClientRect();
|
||
return { x: r.left - d.left + r.width * fx_, y: r.top - d.top + r.height * fy };
|
||
}
|
||
var OUT = 'cubic-bezier(0.2, 0.8, 0.3, 1)', IN = 'cubic-bezier(0.6, 0, 0.9, 0.5)', IO = 'cubic-bezier(0.4, 0, 0.3, 1)';
|
||
// тело: [доля, сдвиг X в % своей ширины, сдвиг Y в %, наклон°, изинг]
|
||
function body(el, keys, ms) {
|
||
el.animate(keys.map(function (k) {
|
||
var f = { offset: k[0], translate: k[1] + '% ' + (k[2] || 0) + '%', rotate: (k[3] || 0) + 'deg' };
|
||
if (k[4]) f.easing = k[4];
|
||
return f;
|
||
}), { duration: ms, easing: IO });
|
||
}
|
||
// рука: [доля, поворот°, масштаб, изинг]
|
||
function arm(el, keys, ms) {
|
||
if (!el) return;
|
||
el.animate(keys.map(function (k) {
|
||
var f = { offset: k[0], transform: 'rotate(' + k[1] + 'deg) scale(' + (k[2] || 1) + ')' };
|
||
if (k[3]) f.easing = k[3];
|
||
return f;
|
||
}), { duration: ms, easing: IO });
|
||
}
|
||
function shake(power) {
|
||
duel.animate([{ translate: '0 0' }, { translate: -power + 'px ' + power / 2 + 'px' }, { translate: power + 'px ' + -power / 2 + 'px' },
|
||
{ translate: -power / 2 + 'px ' + power / 4 + 'px' }, { translate: '0 0' }], { duration: 280 });
|
||
}
|
||
function flashVs(k) {
|
||
if (vs) vs.animate([{ filter: 'drop-shadow(0 0 18px rgba(155,123,255,.8))' }, { filter: 'drop-shadow(0 0 40px #fff) brightness(' + k + ')' },
|
||
{ filter: 'drop-shadow(0 0 18px rgba(155,123,255,.8))' }], { duration: 420 });
|
||
}
|
||
function mark(el, cls, ms) { el.classList.add(cls); setTimeout(function () { el.classList.remove(cls) }, ms) }
|
||
|
||
/* ── эффекты на холсте ── */
|
||
function add(o) { o.t0 = performance.now(); fx.push(o); if (!raf) raf = requestAnimationFrame(draw) }
|
||
function sparks(p, n, speed, colors, up) {
|
||
for (var i = 0; i < n; i++) {
|
||
var a = Math.random() * Math.PI * 2, v = speed * (0.35 + Math.random() * 0.8);
|
||
add({ k: 'spark', x: p.x, y: p.y, vx: Math.cos(a) * v, vy: Math.sin(a) * v - (up || 0), life: 380 + Math.random() * 380, c: colors[i % colors.length] });
|
||
}
|
||
}
|
||
function draw(now) {
|
||
raf = 0;
|
||
g.setTransform(dpr, 0, 0, dpr, 0, 0); g.clearRect(0, 0, W, H);
|
||
g.lineCap = 'round'; g.globalCompositeOperation = 'lighter';
|
||
var U = Math.min(W, H);
|
||
fx = fx.filter(function (o) {
|
||
var t = now - o.t0;
|
||
if (o.k === 'slash') { // три следа когтей: прорисовываются за 110 мс и гаснут
|
||
var p = Math.min(1, t / 110), a = t < 110 ? 1 : Math.max(0, 1 - (t - 110) / 420);
|
||
for (var i = -1; i <= 1; i++) {
|
||
var ox = -o.ny * i * o.gap, oy = o.nx * i * o.gap;
|
||
var sx = o.x - o.nx * o.len / 2 + ox, sy = o.y - o.ny * o.len / 2 + oy;
|
||
var ex = sx + o.nx * o.len * p, ey = sy + o.ny * o.len * p, cx = (sx + ex) / 2 - o.ny * o.len * 0.12, cy = (sy + ey) / 2 + o.nx * o.len * 0.12;
|
||
[[16, 'rgba(255,40,80,' + 0.18 * a + ')'], [7, 'rgba(255,70,100,' + 0.55 * a + ')'], [2.2, 'rgba(255,235,240,' + a + ')']].forEach(function (s) {
|
||
g.beginPath(); g.moveTo(sx, sy); g.quadraticCurveTo(cx, cy, ex, ey); g.lineWidth = s[0]; g.strokeStyle = s[1]; g.stroke();
|
||
});
|
||
}
|
||
return t < 530;
|
||
}
|
||
if (o.k === 'shield') { // блок: голубой шестигранник щита
|
||
var q = Math.min(1, t / 460), r = o.r * (0.85 + 0.3 * q), al = 1 - q;
|
||
[[18, 'rgba(79,209,255,' + 0.16 * al + ')'], [6, 'rgba(79,209,255,' + 0.55 * al + ')'], [2, 'rgba(230,250,255,' + al + ')']].forEach(function (s) {
|
||
g.beginPath();
|
||
for (var j = 0; j <= 6; j++) { var an = j / 6 * Math.PI * 2 + Math.PI / 6; g[j ? 'lineTo' : 'moveTo'](o.x + Math.cos(an) * r, o.y + Math.sin(an) * r) }
|
||
g.lineWidth = s[0]; g.strokeStyle = s[1]; g.stroke();
|
||
});
|
||
return t < 460;
|
||
}
|
||
if (o.k === 'burst') { // попадание: вспышка, кольца и лучи
|
||
var b = Math.min(1, t / 480), fade = 1 - b;
|
||
if (t < 200) {
|
||
var gr = g.createRadialGradient(o.x, o.y, 0, o.x, o.y, Math.max(W, H) * 0.6);
|
||
gr.addColorStop(0, 'rgba(235,248,255,' + 0.5 * (1 - t / 200) + ')'); gr.addColorStop(1, 'rgba(79,209,255,0)');
|
||
g.fillStyle = gr; g.fillRect(0, 0, W, H);
|
||
}
|
||
var R = U * 0.3 * (0.2 + 1.3 * Math.pow(b, 0.6));
|
||
g.beginPath(); g.arc(o.x, o.y, R, 0, Math.PI * 2); g.lineWidth = 3 + 8 * fade; g.strokeStyle = 'rgba(200,240,255,' + 0.85 * fade + ')'; g.stroke();
|
||
g.beginPath(); g.arc(o.x, o.y, R * 0.7, 0, Math.PI * 2); g.lineWidth = 10 * fade; g.strokeStyle = 'rgba(255,59,92,' + 0.35 * fade + ')'; g.stroke();
|
||
for (var k = 0; k < 14; k++) {
|
||
var an2 = k / 14 * Math.PI * 2 + o.rot, r0 = R * 0.35, r1 = R * (0.9 + 0.5 * ((k * 7) % 5) / 5);
|
||
g.beginPath(); g.moveTo(o.x + Math.cos(an2) * r0, o.y + Math.sin(an2) * r0); g.lineTo(o.x + Math.cos(an2) * r1, o.y + Math.sin(an2) * r1);
|
||
g.lineWidth = 2.4 * fade + 0.5; g.strokeStyle = 'rgba(230,250,255,' + fade + ')'; g.stroke();
|
||
}
|
||
return t < 480;
|
||
}
|
||
if (o.k === 'spark') {
|
||
var s2 = t / 1000, life = 1 - t / o.life, x = o.x + o.vx * s2, y = o.y + o.vy * s2 + 900 * s2 * s2;
|
||
g.beginPath(); g.moveTo(x, y); g.lineTo(x - o.vx * 0.03, y - (o.vy + 1800 * s2) * 0.03);
|
||
g.lineWidth = 2; g.strokeStyle = o.c.replace('A', Math.max(0, life).toFixed(2)); g.stroke();
|
||
return t < o.life;
|
||
}
|
||
return false;
|
||
});
|
||
g.globalCompositeOperation = 'source-over';
|
||
if (fx.length) raf = requestAnimationFrame(draw);
|
||
else g.clearRect(0, 0, W, H);
|
||
}
|
||
var BLUE = ['rgba(79,209,255,A)', 'rgba(230,250,255,A)'], RED = ['rgba(255,59,92,A)', 'rgba(255,200,210,A)'];
|
||
// кончики: когти РКН-тян — левый край её передней руки, кулак Запрет-тян — правый край её ударной
|
||
function claw() { return at(rkClaw, 0.08, 0.5) }
|
||
function fist() { return at(ztPunch, 0.93, 0.62) }
|
||
|
||
/* ── хореография ── */
|
||
var P = 7200;
|
||
var EV = [
|
||
[150, function () { // РКН-тян: замах
|
||
body(rk, [[0, 0, 0, 0], [1, 3, 0, 3]], 450);
|
||
arm(rkClaw, [[0, 0], [1, 24, 1.04, IN]], 450);
|
||
}],
|
||
[600, function () { // бросок и удар когтями сверху вниз
|
||
body(rk, [[0, 3, 0, 3], [0.2, -11, 1, -5, OUT], [0.55, -9, 1, -4], [1, 0, 0, 0]], 1000);
|
||
arm(rkClaw, [[0, 24, 1.04], [0.16, -30, 1.12, OUT], [0.5, -22, 1.06], [1, 0, 1]], 1000);
|
||
}],
|
||
[620, function () { body(zt, [[0, 0, 0, 0], [0.3, -8, -1, -9, OUT], [0.65, -7, -1, -7], [1, 0, 0, 0]], 850) }], // Запрет-тян уклоняется
|
||
[760, function () { // промах: следы когтей в воздухе
|
||
var p = claw(), ang = 1.15;
|
||
add({ k: 'slash', x: p.x - W * 0.02, y: p.y + H * 0.04, nx: Math.cos(ang), ny: Math.sin(ang), len: H * 0.42, gap: H * 0.04 });
|
||
shake(3);
|
||
}],
|
||
[2200, function () { // колющий удар и блок
|
||
body(rk, [[0, 0, 0, 0], [0.35, 2, 0, 2], [0.55, -9, 0, -3, OUT], [1, 0, 0, 0]], 1100);
|
||
arm(rkClaw, [[0, 0, 1], [0.35, 8, 0.96], [0.55, -4, 1.22, OUT], [0.8, -2, 1.14], [1, 0, 1]], 1100);
|
||
arm(ztGuard, [[0, 0, 1], [0.4, -16, 1.06, OUT], [0.75, -14, 1.05], [1, 0, 1]], 1100);
|
||
body(zt, [[0, 0, 0, 0], [0.55, 0, 0, 0], [0.7, -3, 0, -3, OUT], [1, 0, 0, 0]], 1100);
|
||
}],
|
||
[2810, function () {
|
||
var a = claw(), b = at(ztGuard, 0.85, 0.2), p = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
||
add({ k: 'shield', x: p.x, y: p.y, r: H * 0.13 });
|
||
sparks(p, 18, 520, BLUE.concat(RED), 120); shake(4); flashVs(1.5);
|
||
}],
|
||
[3500, function () { // контратака кулаком
|
||
body(zt, [[0, 0, 0, 0], [0.25, -3, 0, -3, IN], [0.42, 22, -1, 6, OUT], [0.62, 18, -1, 5], [1, 0, 0, 0]], 1300);
|
||
arm(ztPunch, [[0, 0, 1], [0.25, 10, 0.94, IN], [0.42, -5, 1.38, OUT], [0.62, -4, 1.3], [1, 0, 1]], 1300);
|
||
arm(ztGuard, [[0, 0, 1], [0.42, -8, 1.04], [1, 0, 1]], 1300);
|
||
}],
|
||
[4060, function () { // попадание
|
||
var p = fist();
|
||
add({ k: 'burst', x: p.x, y: p.y, rot: Math.random() });
|
||
sparks(p, 34, 900, BLUE, 200); sparks(p, 10, 600, RED, 100);
|
||
shake(10); flashVs(2.2); duel._boostUntil = performance.now() + 520;
|
||
body(rk, [[0, 0, 0, 0], [0.12, 10, -1, 9, OUT], [0.45, 7, 0, 6], [1, 0, 0, 0]], 1200);
|
||
arm(rkClaw, [[0, 0, 1], [0.15, 32, 1.05, OUT], [0.5, 18, 1.02], [1, 0, 1]], 1200);
|
||
arm(rkBack, [[0, 0, 1], [0.15, -18, 1.04, OUT], [1, 0, 1]], 1200);
|
||
mark(rk, 'zf-hit', 520);
|
||
}],
|
||
[5400, function () { // ярость РКН-тян, Запрет-тян дразнится
|
||
arm(rkClaw, [[0, 0, 1], [0.25, 26, 1.1, OUT], [0.4, 20, 1.06], [0.55, 26, 1.1], [1, 0, 1]], 1300);
|
||
arm(rkBack, [[0, 0, 1], [0.25, -24, 1.08, OUT], [0.4, -18, 1.04], [0.55, -24, 1.08], [1, 0, 1]], 1300);
|
||
body(rk, [[0, 0, 0, 0], [0.3, 1, -2, -2], [1, 0, 0, 0]], 1300);
|
||
body(zt, [[0, 0, 0, 0], [0.3, 0, -4, -2, OUT], [0.5, 0, 0, 0], [0.7, 0, -2, -1], [1, 0, 0, 0]], 1000);
|
||
arm(ztGuard, [[0, 0, 1], [0.3, -12, 1.05], [0.6, -4, 1.02], [1, 0, 1]], 1000);
|
||
}]
|
||
];
|
||
|
||
var t0 = 0, last = -1, on = false, loop = 0;
|
||
function tick(now) {
|
||
loop = 0;
|
||
if (!on || document.hidden || root.classList.contains('zpm-intro-on')) return;
|
||
if (!t0) { t0 = now; last = -1 }
|
||
var ct = (now - t0) % P;
|
||
if (ct < last) last = -1; // новый круг
|
||
EV.forEach(function (e) { if (e[0] > last && e[0] <= ct) e[1]() });
|
||
last = ct;
|
||
loop = requestAnimationFrame(tick);
|
||
}
|
||
function start() { if (!loop && on) { size(); t0 = 0; loop = requestAnimationFrame(tick) } }
|
||
new IntersectionObserver(function (es) {
|
||
on = es[0].isIntersecting && duel.classList.contains('zd-on');
|
||
if (on) start(); else if (loop) { cancelAnimationFrame(loop); loop = 0 }
|
||
}, { threshold: 0.25 }).observe(duel);
|
||
// дуэль включается (zd-on) чуть позже появления на экране — подхватываем после выезда бойцов
|
||
var mo = new MutationObserver(function () { if (duel.classList.contains('zd-on') && !on) { on = true; setTimeout(start, 1400); mo.disconnect() } });
|
||
mo.observe(duel, { attributes: true, attributeFilter: ['class'] });
|
||
document.addEventListener('visibilitychange', function () { if (!document.hidden && on) { t0 = 0; start() } });
|
||
})();
|
||
|
||
/* ── Появление блоков при прокрутке ─────────────────────────────────── */
|
||
(function () {
|
||
if (!window.IntersectionObserver || (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches)) return;
|
||
var groups = [
|
||
['.zsec-head', ''], ['.zp-kpi', 'rv-pop'], ['.zp-panel', ''], ['.zc', 'rv-card'],
|
||
['.zt-list li', 'rv-side'], ['.zrn', 'rv-side'], ['.zfd', ''], ['.zci-run', ''],
|
||
['.zci-points > div', ''], ['.zj-help', 'rv-card'], ['.zj-join > h2', ''], ['.zj-hs li', 'rv-side'], ['.zj-cta', ''],
|
||
['.zci-points > .zb', 'rv-pop']
|
||
];
|
||
var io = new IntersectionObserver(function (es) {
|
||
es.forEach(function (e) {
|
||
if (!e.isIntersecting) return;
|
||
e.target.classList.remove('rv-wait'); e.target.classList.add('rv-in'); io.unobserve(e.target);
|
||
});
|
||
}, { rootMargin: '0px 0px -8% 0px' });
|
||
function words(h) {
|
||
var w = 0;
|
||
h.innerHTML = h.textContent.split(/(\s+)/).map(function (p) {
|
||
return /^\s+$/.test(p) || !p ? p : '<span class="rw" style="--w:' + (w++) + '">' + p + '</span>';
|
||
}).join('');
|
||
h.classList.add('rv-words');
|
||
}
|
||
groups.forEach(function (g) {
|
||
document.querySelectorAll('.zpm-home ' + g[0]).forEach(function (el) {
|
||
if (el.getBoundingClientRect().top < innerHeight) return; // уже на экране — не прячем
|
||
var sib = el.parentNode ? Array.prototype.filter.call(el.parentNode.children, function (c) { return c.matches(g[0].split(' ').pop()) }) : [el];
|
||
el.style.setProperty('--rv', Math.min(8, sib.indexOf(el)));
|
||
if (g[1]) el.classList.add(g[1]);
|
||
var h = el.matches('h2') ? el : el.querySelector(':scope > h2');
|
||
if (h && !h.querySelector('*')) words(h);
|
||
el.classList.add('rv-wait');
|
||
io.observe(el);
|
||
});
|
||
});
|
||
})();
|
||
|
||
/* ── Живые цифры в шапке ───────────────────────────────────────────── */
|
||
var hs = document.getElementById('zh-stats');
|
||
if (hs) loadStats().then(function (d) {
|
||
if (d.source !== 'commits') return;
|
||
var t = d.totals || {};
|
||
hs.hidden = false;
|
||
hs.querySelectorAll('[data-hs]').forEach(function (b) {
|
||
var v = t[b.getAttribute('data-hs')];
|
||
if (v == null) { b.parentNode.hidden = true; return }
|
||
var go = function () { countUp(b, v, 1800) };
|
||
if (root.classList.contains('zpm-intro-on')) {
|
||
var wait = setInterval(function () { if (!root.classList.contains('zpm-intro-on')) { clearInterval(wait); go() } }, 300);
|
||
} else setTimeout(go, 1500);
|
||
});
|
||
}).catch(function () {});
|
||
|
||
/* ── Персонажи: следят за курсором и реагируют на касание ────────────── */
|
||
(function () {
|
||
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||
var rigs = Array.prototype.slice.call(document.querySelectorAll('.zrig'));
|
||
if (!rigs.length) return;
|
||
var mx = innerWidth / 2, my = innerHeight / 2, raf = 0;
|
||
var state = rigs.map(function () { return { x: 0, y: 0 } });
|
||
function tick() {
|
||
raf = 0;
|
||
var moving = false;
|
||
rigs.forEach(function (r, i) {
|
||
var b = rects[i]; // положение кэшировано: чтение rect каждый кадр заставляло браузер пересчитывать раскладку
|
||
if (!b || !b.width || b.bottom < 0 || b.top > innerHeight) return;
|
||
var tx = Math.max(-1, Math.min(1, (mx - (b.left + b.width / 2)) / (b.width * 0.6)));
|
||
var ty = Math.max(-1, Math.min(1, (my - (b.top + b.height / 2)) / (b.height * 0.6)));
|
||
var s = state[i];
|
||
s.x += (tx - s.x) * 0.08; s.y += (ty - s.y) * 0.08;
|
||
if (Math.abs(tx - s.x) > 0.002 || Math.abs(ty - s.y) > 0.002) moving = true;
|
||
r.style.setProperty('--px', s.x.toFixed(3));
|
||
r.style.setProperty('--py', s.y.toFixed(3));
|
||
});
|
||
if (moving) raf = requestAnimationFrame(tick);
|
||
}
|
||
var rects = [], rectsAt = 0;
|
||
function measure() { rects = rigs.map(function (r) { return r.getBoundingClientRect() }); rectsAt = performance.now() }
|
||
function kick() { if (!raf) { if (performance.now() - rectsAt > 300) measure(); raf = requestAnimationFrame(tick) } }
|
||
addEventListener('pointermove', function (e) { mx = e.clientX; my = e.clientY; kick() }, { passive: true });
|
||
addEventListener('scroll', function () { rectsAt = 0; kick() }, { passive: true });
|
||
addEventListener('resize', function () { rectsAt = 0 });
|
||
function poke(r) {
|
||
if (r.classList.contains('zr-poke')) return;
|
||
r.classList.add('zr-poke');
|
||
setTimeout(function () { r.classList.remove('zr-poke') }, 800);
|
||
}
|
||
rigs.forEach(function (r) {
|
||
var last = 0;
|
||
r.addEventListener('pointerenter', function () { if (Date.now() - last > 1500) { last = Date.now(); poke(r) } });
|
||
r.addEventListener('pointerdown', function () { poke(r) });
|
||
});
|
||
})();
|
||
|
||
|
||
/* ── Молния дуэли: фрактальный разряд на canvas ─────────────────────── */
|
||
(function () {
|
||
var duel = document.getElementById('zpm-duel');
|
||
var cv = duel && duel.querySelector('.zd-canvas');
|
||
if (!cv || !cv.getContext || (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches)) return;
|
||
var g = cv.getContext('2d'), W = 0, H = 0, dpr = 1, horiz = false, visible = false, raf = 0, nextShape = 0, bolts = [], flick = 1;
|
||
duel.classList.add('zd-live');
|
||
|
||
function size() {
|
||
var r = cv.getBoundingClientRect();
|
||
dpr = Math.min(2, window.devicePixelRatio || 1);
|
||
W = r.width; H = r.height;
|
||
cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr);
|
||
horiz = W > H;
|
||
}
|
||
// смещение средней точки: ломаная с убывающим разбросом
|
||
function crack(x1, y1, x2, y2, rough, depth, out) {
|
||
if (depth === 0) { out.push([x2, y2]); return }
|
||
var mx = (x1 + x2) / 2, my = (y1 + y2) / 2, len = Math.hypot(x2 - x1, y2 - y1);
|
||
var nx = -(y2 - y1) / len, ny = (x2 - x1) / len, off = (Math.random() - 0.5) * len * rough;
|
||
mx += nx * off; my += ny * off;
|
||
crack(x1, y1, mx, my, rough, depth - 1, out);
|
||
crack(mx, my, x2, y2, rough, depth - 1, out);
|
||
}
|
||
function shape(boost) {
|
||
var L = horiz ? W : H, T = horiz ? H : W, main = [];
|
||
var a = horiz ? [-10, H / 2] : [W / 2, -10], b = horiz ? [W + 10, H / 2] : [W / 2, H + 10];
|
||
main.push(a); crack(a[0], a[1], b[0], b[1], 0.32, 8, main);
|
||
// держим ствол в пределах полосы
|
||
var ax = horiz ? 1 : 0, c = T / 2, dev = 1;
|
||
main.forEach(function (p) { dev = Math.max(dev, Math.abs(p[ax] - c)) });
|
||
var f = Math.min(1, T * 0.38 / dev); // сжимаем разброс равномерно, а не обрезаем по краю
|
||
main.forEach(function (p) { p[ax] = c + (p[ax] - c) * f });
|
||
var list = [{ pts: main, w: 1 }];
|
||
var nb = (boost ? 7 : 3) + Math.floor(Math.random() * 3);
|
||
for (var i = 0; i < nb; i++) {
|
||
var s = main[4 + Math.floor(Math.random() * (main.length - 8))];
|
||
var ang = (horiz ? Math.PI / 2 : 0) + (Math.random() < 0.5 ? -1 : 1) * (0.5 + Math.random() * 0.9) + (horiz ? 0 : (Math.random() < 0.5 ? 0 : Math.PI));
|
||
var bl = L * (0.08 + Math.random() * (boost ? 0.22 : 0.14));
|
||
var e = [s[0] + Math.sin(ang) * bl * (horiz ? 1 : 0.9), s[1] + Math.cos(ang) * bl];
|
||
if (!horiz) e = [s[0] + (Math.random() < 0.5 ? -1 : 1) * bl * 0.6, s[1] + bl * (Math.random() < 0.3 ? -0.5 : 0.8)];
|
||
else e = [s[0] + bl * (Math.random() < 0.5 ? -0.6 : 0.8), s[1] + (Math.random() < 0.5 ? -1 : 1) * bl * 0.6];
|
||
var pts = [s]; crack(s[0], s[1], e[0], e[1], 0.45, 5, pts);
|
||
list.push({ pts: pts, w: 0.45 });
|
||
}
|
||
return list;
|
||
}
|
||
function stroke(pts, width, color, blur) {
|
||
g.beginPath(); g.moveTo(pts[0][0], pts[0][1]);
|
||
for (var i = 1; i < pts.length; i++) g.lineTo(pts[i][0], pts[i][1]);
|
||
g.lineWidth = width; g.strokeStyle = color; g.stroke(); // без shadowBlur: он дорогой, свечение — широкими слоями
|
||
}
|
||
function frame(now) {
|
||
raf = 0;
|
||
if (!visible || document.hidden) return;
|
||
if (root.classList.contains('zpm-intro-on')) { setTimeout(kick, 500); return }
|
||
var boost = false;
|
||
if (duel._boostUntil) boost = now < duel._boostUntil; // всплеск разряда в момент удара — его ставит хореография драки
|
||
if (now > nextShape) {
|
||
bolts = shape(boost);
|
||
nextShape = now + (boost ? 45 : 70 + Math.random() * 60);
|
||
flick = Math.random() < 0.06 ? 0.25 : 0.75 + Math.random() * 0.25;
|
||
}
|
||
g.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
g.clearRect(0, 0, W, H);
|
||
g.lineCap = 'round'; g.lineJoin = 'round';
|
||
g.globalCompositeOperation = 'lighter';
|
||
var k = (boost ? 1.9 : 1.2) * flick, sh = horiz ? [0, 2] : [2, 0];
|
||
bolts.forEach(function (b) {
|
||
var w = b.w * k;
|
||
g.save(); g.translate(-sh[0], -sh[1]); stroke(b.pts, 26 * w, 'rgba(79,209,255,' + (0.06 * k) + ')'); stroke(b.pts, 12 * w, 'rgba(79,209,255,' + (0.16 * k) + ')'); g.restore();
|
||
g.save(); g.translate(sh[0], sh[1]); stroke(b.pts, 26 * w, 'rgba(255,59,92,' + (0.06 * k) + ')'); stroke(b.pts, 12 * w, 'rgba(255,59,92,' + (0.15 * k) + ')'); g.restore();
|
||
stroke(b.pts, 3.2 * w, 'rgba(190,225,255,' + (0.8 * k) + ')', 12);
|
||
stroke(b.pts, 1.3 * w + 0.4, 'rgba(255,255,255,' + Math.min(1, k) + ')', 4);
|
||
});
|
||
g.globalCompositeOperation = 'source-over';
|
||
raf = requestAnimationFrame(frame);
|
||
}
|
||
function kick() { if (!raf && visible) raf = requestAnimationFrame(frame) }
|
||
size();
|
||
addEventListener('resize', function () { size(); nextShape = 0 });
|
||
document.addEventListener('visibilitychange', kick);
|
||
new IntersectionObserver(function (es) { visible = es[0].isIntersecting; if (visible) { size(); kick() } }).observe(duel);
|
||
})();
|
||
|
||
/* ── Досмотр на ТСПУ: релизы из API и шум байтов в «том, что видит цензор» ── */
|
||
(function () {
|
||
var net = document.getElementById('zr-net');
|
||
if (!net) return;
|
||
var rows = net.querySelectorAll('.zrn[data-repo]');
|
||
function ago(ts) {
|
||
var d = Math.floor((Date.now() - ts) / 864e5);
|
||
if (d < 1) return 'сегодня';
|
||
if (d < 2) return 'вчера';
|
||
if (d < 14) return d + ' ' + plural(d, 'день', 'дня', 'дней') + ' назад';
|
||
if (d < 60) { var w = Math.floor(d / 7); return w + ' ' + plural(w, 'неделю', 'недели', 'недель') + ' назад' }
|
||
var m = Math.floor(d / 30); return m + ' ' + plural(m, 'месяц', 'месяца', 'месяцев') + ' назад';
|
||
}
|
||
function load() {
|
||
rows.forEach(function (b) {
|
||
fetch(net.getAttribute('data-api') + b.getAttribute('data-repo') + '/releases/latest')
|
||
.then(function (r) { return r.ok ? r.json() : null })
|
||
.then(function (r) {
|
||
if (!r || !r.tag_name) return;
|
||
var ts = Date.parse(r.published_at || r.created_at);
|
||
var dl = (r.assets || []).reduce(function (a, x) { return a + (x.download_count || 0) }, 0);
|
||
b.querySelector('[data-f="ver"]').textContent = 'v' + String(r.tag_name).replace(/^(zsg-|v)/i, '');
|
||
if (ts) b.querySelector('[data-f="age"]').textContent = ago(ts);
|
||
if (dl) b.querySelector('[data-f="dl"]').textContent = '↓ ' + short(dl);
|
||
if (ts && Date.now() - ts < 3 * 864e5) b.classList.add('zrn-fresh');
|
||
}).catch(function () {});
|
||
});
|
||
}
|
||
if (!window.IntersectionObserver) { if (window.fetch) load(); return }
|
||
if (window.fetch) {
|
||
var io = new IntersectionObserver(function (es) { if (es[0].isIntersecting) { io.disconnect(); load() } }, { rootMargin: '600px 0px' });
|
||
io.observe(net);
|
||
}
|
||
/* пока блок на экране, пара байтов в каждой строке меняется — цензор видит только шум */
|
||
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||
var hex = net.querySelectorAll('[data-hex]'), HEX = '0123456789abcdef', timer = 0;
|
||
function noise() {
|
||
hex.forEach(function (el) {
|
||
var bytes = el.textContent.split(' ');
|
||
for (var k = 0; k < 2; k++) bytes[Math.floor(Math.random() * bytes.length)] = HEX[Math.floor(Math.random() * 16)] + HEX[Math.floor(Math.random() * 16)];
|
||
el.textContent = bytes.join(' ');
|
||
});
|
||
}
|
||
new IntersectionObserver(function (es) {
|
||
clearInterval(timer);
|
||
if (es[0].isIntersecting) timer = setInterval(noise, 140);
|
||
}).observe(net);
|
||
})();
|
||
|
||
/* ── Лаборатория доверия: сборка по шагам и сверка sha256 ───────────── */
|
||
(function () {
|
||
var lab = document.getElementById('zci-lab');
|
||
if (!lab || !window.IntersectionObserver || (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches)) return;
|
||
var items = lab.querySelectorAll('.zci-log li'), codes = lab.querySelectorAll('.zci-h code'), stamp = lab.querySelector('.zci-ok');
|
||
var HEX = '0123456789abcdef', visible = false, busy = false, timers = [];
|
||
function later(fn, ms) { timers.push(setTimeout(fn, ms)) }
|
||
function hex(n) { var s = ''; for (var i = 0; i < n; i++) s += HEX[Math.floor(Math.random() * 16)]; return s }
|
||
function run() {
|
||
if (busy || !visible) return;
|
||
busy = true;
|
||
lab.classList.add('zci-live'); lab.classList.remove('zci-match'); stamp.classList.remove('on');
|
||
var hash = hex(64);
|
||
codes.forEach(function (c) { c.innerHTML = hash.split('').map(function (_, n) { return '<i style="--n:' + n + '">·</i>' }).join('') });
|
||
items.forEach(function (li) { li.className = 'pending' });
|
||
var t = 300;
|
||
items.forEach(function (li, i) {
|
||
later(function () { li.className = 'run' }, t);
|
||
t += i === 2 ? 1500 : 600 + Math.random() * 300;
|
||
later(function () { li.className = 'ok' }, t);
|
||
});
|
||
later(function () { scramble(hash) }, t + 200);
|
||
}
|
||
function scramble(hash) {
|
||
var spans = [codes[0].children, codes[1].children], locked = [0, -14], tick = setInterval(function () {
|
||
for (var k = 0; k < 2; k++) {
|
||
locked[k] += 2;
|
||
for (var i = 0; i < 64; i++) {
|
||
var s = spans[k][i];
|
||
if (i < locked[k]) { if (!s.classList.contains('lk')) { s.textContent = hash[i]; s.classList.add('lk') } }
|
||
else s.textContent = HEX[Math.floor(Math.random() * 16)];
|
||
}
|
||
}
|
||
if (locked[1] >= 64) {
|
||
clearInterval(tick);
|
||
lab.classList.add('zci-match');
|
||
later(function () { stamp.classList.add('on') }, 250);
|
||
later(function () { busy = false; run() }, 5200);
|
||
}
|
||
}, 45);
|
||
timers.push({ clear: function () { clearInterval(tick) } });
|
||
}
|
||
new IntersectionObserver(function (es) {
|
||
visible = es[0].isIntersecting;
|
||
if (visible) run();
|
||
}, { threshold: 0.35 }).observe(lab);
|
||
})();
|
||
|
||
/* ── Мини-игра «Спасите пакеты» ──────────────────────────────────────── */
|
||
(function () {
|
||
var box = document.getElementById('zg-packets');
|
||
var cv = box && box.querySelector('canvas');
|
||
if (!cv || !cv.getContext || !window.IntersectionObserver) return;
|
||
var g = cv.getContext('2d'), W = 0, H = 0, dpr = 1, wireY = 0, visible = false, raf = 0, last = 0;
|
||
var pk = [], claws = [], fx = [], nextPk = 0, nextClaw = 1.5, clock = 0;
|
||
var score = { saved: 0, lost: 0, best: 0 }, toast = box.querySelector('.zg-toast'), shown = {};
|
||
try { score.best = +localStorage.getItem('zg-best') || 0 } catch (e) {}
|
||
function ui() { for (var k in score) box.querySelector('[data-g="' + k + '"]').textContent = score[k] }
|
||
ui();
|
||
function size() {
|
||
var r = cv.getBoundingClientRect(); dpr = Math.min(2, devicePixelRatio || 1);
|
||
W = r.width; H = r.height; cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr); wireY = H * 0.68;
|
||
}
|
||
function spawn() { pk.push({ x: -20, v: 70 + Math.random() * 60 + Math.min(60, score.saved * 1.5), s: 0, out: 0, grab: null }) }
|
||
function attack() {
|
||
var cand = pk.filter(function (p) { return !p.s && !p.grab && p.x > W * 0.25 && p.x < W * 0.8 });
|
||
if (!cand.length) return;
|
||
var t = cand[Math.floor(Math.random() * cand.length)];
|
||
claws.push({ target: t, x: t.x + t.v * 0.9, st: 'warn', t: 0 });
|
||
}
|
||
function burst(x, y, col, n) { for (var i = 0; i < n; i++) { var a = Math.random() * 6.28, v = 60 + Math.random() * 140; fx.push({ x: x, y: y, vx: Math.cos(a) * v, vy: Math.sin(a) * v, life: 0.6, col: col }) } }
|
||
function milestone() {
|
||
var m = [10, 25, 50, 100].filter(function (n) { return score.saved >= n && !shown[n] })[0];
|
||
if (!m) return;
|
||
shown[m] = 1;
|
||
toast.querySelector('b').textContent = m + ' ' + plural(m, 'пакет спасён!', 'пакета спасены!', 'пакетов спасено!');
|
||
toast.hidden = false; clearTimeout(toast._t); toast._t = setTimeout(function () { toast.hidden = true }, 7000);
|
||
}
|
||
function drawClaw(x, y, a) {
|
||
g.save(); g.translate(x, y); g.globalAlpha = a;
|
||
g.fillStyle = 'rgba(255,43,79,.18)'; g.beginPath(); g.ellipse(0, -30, 44, 50, 0, 0, 6.28); g.fill(); // ореол без shadowBlur
|
||
g.fillStyle = '#e9b8a4'; g.beginPath(); g.ellipse(0, -46, 26, 30, 0, 0, 6.28); g.fill();
|
||
[-18, -6, 6, 18].forEach(function (dx, i) {
|
||
g.fillStyle = '#e9b8a4'; g.beginPath(); g.ellipse(dx, -18, 6, 16, dx / 60, 0, 6.28); g.fill();
|
||
g.fillStyle = '#1a0a18'; g.beginPath(); g.moveTo(dx - 5, -6); g.quadraticCurveTo(dx + dx * 0.15, 14, dx + dx * 0.25, 20); g.quadraticCurveTo(dx + 6, 6, dx + 5, -6); g.fill();
|
||
});
|
||
g.restore();
|
||
}
|
||
function frame(now) {
|
||
raf = 0; if (!visible || document.hidden) { last = 0; return }
|
||
if (root.classList.contains('zpm-intro-on')) { last = 0; setTimeout(function () { if (!raf) raf = requestAnimationFrame(frame) }, 500); return }
|
||
var dt = last ? Math.min(0.05, (now - last) / 1000) : 0; last = now; clock += dt;
|
||
if (clock > nextPk) { spawn(); nextPk = clock + 0.55 + Math.random() * 0.6 }
|
||
if (clock > nextClaw) { attack(); nextClaw = clock + Math.max(0.9, 2.6 - score.saved * 0.03) + Math.random() * 1.2 }
|
||
g.setTransform(dpr, 0, 0, dpr, 0, 0); g.clearRect(0, 0, W, H);
|
||
// провод
|
||
var grd = g.createLinearGradient(0, 0, W, 0); grd.addColorStop(0, 'rgba(79,209,255,0)'); grd.addColorStop(0.1, 'rgba(79,209,255,.7)'); grd.addColorStop(0.9, 'rgba(155,123,255,.7)'); grd.addColorStop(1, 'rgba(155,123,255,0)');
|
||
g.strokeStyle = grd; g.lineWidth = 3; g.beginPath(); g.moveTo(0, wireY); g.lineTo(W, wireY); g.stroke();
|
||
// когти
|
||
claws.forEach(function (c) {
|
||
c.t += dt;
|
||
if (c.st === 'warn') {
|
||
g.strokeStyle = 'rgba(255,59,92,' + (0.4 + 0.4 * Math.sin(c.t * 30)) + ')'; g.setLineDash([6, 6]); g.lineWidth = 2;
|
||
g.beginPath(); g.moveTo(c.x, 0); g.lineTo(c.x, wireY); g.stroke(); g.setLineDash([]);
|
||
if (c.t > 0.9) { c.st = 'down'; c.t = 0 }
|
||
} else if (c.st === 'down') {
|
||
var y = -40 + (wireY + 40) * Math.min(1, c.t / 0.22);
|
||
drawClaw(c.x, y, 1);
|
||
if (c.t >= 0.22) {
|
||
c.st = 'up'; c.t = 0;
|
||
pk.forEach(function (p) { if (!p.grab && !p.s && Math.abs(p.x - c.x) < 34) { p.grab = c; c.got = p; score.lost++; ui(); burst(p.x, wireY, '#ff3b5c', 14) } });
|
||
pk.forEach(function (p) { if (p.s && Math.abs(p.x - c.x) < 34) { burst(p.x, wireY, '#5b7fff', 10); c.blocked = 1 } });
|
||
}
|
||
} else {
|
||
var y2 = wireY - (wireY + 60) * Math.min(1, c.t / 0.5);
|
||
drawClaw(c.x, y2, 1);
|
||
if (c.got) c.got.y = y2 + 16;
|
||
if (c.t > 0.5) c.dead = 1;
|
||
}
|
||
});
|
||
claws = claws.filter(function (c) { if (c.dead && c.got) c.got.gone = 1; return !c.dead });
|
||
// пакеты
|
||
pk.forEach(function (p) {
|
||
if (!p.grab) p.x += p.v * dt * (p.s ? 1.6 : 1);
|
||
var y = p.grab ? p.y : wireY;
|
||
if (p.s) {
|
||
g.fillStyle = 'rgba(79,209,255,.14)'; g.beginPath(); g.arc(p.x, y, 22, 0, 6.28); g.fill();
|
||
g.strokeStyle = 'rgba(79,209,255,.9)'; g.lineWidth = 2;
|
||
g.beginPath(); g.arc(p.x, y, 17, 0, 6.28); g.stroke();
|
||
}
|
||
g.fillStyle = p.grab ? 'rgba(255,59,92,.22)' : 'rgba(79,209,255,.2)'; g.fillRect(p.x - 14, y - 11, 28, 22);
|
||
g.fillStyle = p.grab ? '#ff5a74' : (p.s ? '#f2eee6' : '#5b7fff');
|
||
g.beginPath(); g.roundRect ? g.roundRect(p.x - 9, y - 7, 18, 14, 4) : g.rect(p.x - 9, y - 7, 18, 14); g.fill();
|
||
if (p.x > W + 20) { p.gone = 1; if (p.s) { score.saved++; if (score.saved > score.best) { score.best = score.saved; try { localStorage.setItem('zg-best', score.best) } catch (e) {} } ui(); milestone() } }
|
||
});
|
||
pk = pk.filter(function (p) { return !p.gone });
|
||
// искры
|
||
fx.forEach(function (f) { f.life -= dt; f.x += f.vx * dt; f.y += f.vy * dt; f.vy += 300 * dt; g.globalAlpha = Math.max(0, f.life / 0.6); g.fillStyle = f.col; g.fillRect(f.x - 2, f.y - 2, 4, 4) });
|
||
g.globalAlpha = 1; fx = fx.filter(function (f) { return f.life > 0 });
|
||
raf = requestAnimationFrame(frame);
|
||
}
|
||
cv.addEventListener('pointerdown', function (e) {
|
||
var r = cv.getBoundingClientRect(), x = e.clientX - r.left, y = e.clientY - r.top, best = null, d = 40;
|
||
pk.forEach(function (p) { var dd = Math.hypot(p.x - x, (p.grab ? p.y : wireY) - y); if (!p.s && !p.grab && dd < d) { d = dd; best = p } });
|
||
if (best) { best.s = 1; burst(best.x, wireY, '#5b7fff', 12) }
|
||
claws.forEach(function (c) { if (c.st !== 'warn' && Math.abs(c.x - x) < 40 && y < wireY + 10) { if (c.got) { c.got.grab = null; c.got.s = 1; c.got = null } c.st = 'up'; c.t = 0.3; burst(c.x, y, '#ffcf5a', 16) } });
|
||
});
|
||
new IntersectionObserver(function (es) { visible = es[0].isIntersecting; if (visible) { size(); if (!raf) raf = requestAnimationFrame(frame) } }).observe(box);
|
||
addEventListener('resize', size);
|
||
document.addEventListener('visibilitychange', function () { if (visible && !raf) raf = requestAnimationFrame(frame) });
|
||
})();
|
||
|
||
/* ── Мини-игра «Сломайте замок РКН» ─────────────────────────────────── */
|
||
(function () {
|
||
var lock = document.getElementById('zlock');
|
||
if (!lock) return;
|
||
var btn = lock.querySelector('.zlock-hit'), fx = lock.querySelector('.zlock-fx'), bar = lock.querySelector('.zlock-hp i');
|
||
var hpEl = lock.querySelector('[data-l="hp"]'), name = lock.querySelector('.zlock-name'), label = lock.querySelector('.zl-label');
|
||
var win = lock.querySelector('.zlock-win'), cracks = lock.querySelectorAll('.zl-crack');
|
||
var LEVELS = [['Сломайте замок РКН', 'РКН', 15], ['Теперь замок ТСПУ', 'ТСПУ', 25], ['Финальный босс: реестр', 'РЕЕСТР', 40]];
|
||
var lvl = 0, hp = 0, max = 0;
|
||
function set(l) {
|
||
lvl = l; max = hp = LEVELS[Math.min(l, 2)][2] + Math.max(0, l - 2) * 15;
|
||
name.textContent = LEVELS[Math.min(l, 2)][0] + (l > 2 ? ' ×' + (l - 1) : '');
|
||
label.textContent = LEVELS[Math.min(l, 2)][1];
|
||
lock.classList.remove('broken'); win.hidden = true; upd();
|
||
}
|
||
function upd() {
|
||
hpEl.textContent = hp; bar.style.width = (hp / max * 100) + '%';
|
||
var k = hp / max; cracks.forEach(function (c) { c.classList.toggle('on', k <= +c.getAttribute('data-at')) });
|
||
}
|
||
function add(el, ms) { fx.appendChild(el); setTimeout(function () { el.remove() }, ms) }
|
||
function hit() {
|
||
if (hp <= 0) return;
|
||
var crit = Math.random() < 0.15, dmg = crit ? 3 : 1;
|
||
hp = Math.max(0, hp - dmg); upd();
|
||
lock.classList.remove('hit'); void lock.offsetWidth; lock.classList.add('hit');
|
||
var n = document.createElement('b'); n.textContent = crit ? 'КРИТ −3!' : '−1'; if (crit) n.className = 'crit';
|
||
n.style.setProperty('--dx', (Math.random() * 60 - 30) + 'px'); add(n, 800);
|
||
for (var i = 0; i < (crit ? 12 : 6); i++) {
|
||
var s = document.createElement('i'), a = Math.random() * 6.28, v = 40 + Math.random() * 70;
|
||
s.style.setProperty('--dx', Math.cos(a) * v + 'px'); s.style.setProperty('--dy', Math.sin(a) * v + 'px'); add(s, 600);
|
||
}
|
||
if (navigator.vibrate) try { navigator.vibrate(crit ? 30 : 10) } catch (e) {}
|
||
if (hp === 0) boom();
|
||
}
|
||
function boom() {
|
||
lock.classList.add('broken');
|
||
var cols = ['#5b7fff', '#9b7bff', '#37e39a', '#ffcf5a', '#ff6bb3'];
|
||
for (var i = 0; i < 60; i++) {
|
||
var c = document.createElement('i'), a = -Math.PI / 2 + (Math.random() - 0.5) * 2.6, v = 90 + Math.random() * 220;
|
||
c.className = 'cf'; c.style.setProperty('--c', cols[i % 5]);
|
||
c.style.setProperty('--dx', Math.cos(a) * v + 'px'); c.style.setProperty('--dy', (Math.sin(a) * v + 160) + 'px'); c.style.setProperty('--r', (Math.random() * 720 - 360) + 'deg');
|
||
add(c, 1600);
|
||
}
|
||
setTimeout(function () { win.hidden = false }, 500);
|
||
try { localStorage.setItem('zlock-lvl', lvl + 1) } catch (e) {}
|
||
}
|
||
btn.addEventListener('click', hit);
|
||
lock.querySelector('.zlock-again').addEventListener('click', function () { set(lvl + 1) });
|
||
var saved = 0; try { saved = +localStorage.getItem('zlock-lvl') || 0 } catch (e) {}
|
||
set(Math.min(saved, 2));
|
||
})();
|
||
|
||
/* ── Своё контекстное меню ──────────────────────────────────────────── */
|
||
(function () {
|
||
if (window.matchMedia && matchMedia('(pointer: coarse)').matches) return; // на телефонах — системное
|
||
var sub = (pulse && pulse.getAttribute('data-heatmap') || '/api/').split('/api/')[0];
|
||
var I = {
|
||
back: '<svg viewBox="0 0 24 24"><path d="M15 5l-7 7 7 7"/></svg>',
|
||
fwd: '<svg viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>',
|
||
reload: '<svg viewBox="0 0 24 24"><path d="M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7"/></svg>',
|
||
top: '<svg viewBox="0 0 24 24"><path d="M12 19V5m-6 6 6-6 6 6"/></svg>',
|
||
open: '<svg viewBox="0 0 24 24"><path d="M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/></svg>',
|
||
copy: '<svg viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"/></svg>',
|
||
link: '<svg viewBox="0 0 24 24"><path d="M10 14a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/></svg>',
|
||
search: '<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg>',
|
||
img: '<svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 16l-5-5-9 9"/></svg>',
|
||
play: '<svg viewBox="0 0 24 24"><path d="M7 4v16l13-8z"/></svg>',
|
||
heart: '<svg viewBox="0 0 24 24"><path d="M12 20s-8-5-8-11a4.5 4.5 0 0 1 8-3 4.5 4.5 0 0 1 8 3c0 6-8 11-8 11z"/></svg>',
|
||
repo: '<svg viewBox="0 0 24 24"><path d="M5 4h11l3 3v13H5z"/><path d="M9 9h6M9 13h6M9 17h4"/></svg>',
|
||
status: '<svg viewBox="0 0 24 24"><path d="M12 3l7 3v6c0 4.5-3 7.8-7 9-4-1.2-7-4.5-7-9V6z"/><path d="M8 12h2l1.5-3 2 6 1.5-3h1"/></svg>',
|
||
lock: '<svg viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0"/></svg>'
|
||
};
|
||
var menu = document.createElement('div'); menu.className = 'zcm'; menu.hidden = true; menu.setAttribute('role', 'menu');
|
||
document.body.appendChild(menu);
|
||
var items = [], cur = -1;
|
||
function toast(msg) { var t = document.createElement('div'); t.className = 'zcm-toast'; t.textContent = msg; document.body.appendChild(t); setTimeout(function () { t.remove() }, 1900) }
|
||
function copy(s, msg) {
|
||
(navigator.clipboard ? navigator.clipboard.writeText(s) : Promise.reject()).then(function () { toast(msg || 'Скопировано') }, function () {
|
||
var ta = document.createElement('textarea'); ta.value = s; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); toast(msg || 'Скопировано') } catch (e) {} ta.remove();
|
||
});
|
||
}
|
||
function close() { menu.hidden = true; cur = -1 }
|
||
function build(e) {
|
||
var a = e.target.closest('a[href]'), im = e.target.closest('img'), sel = String(getSelection() || '').trim();
|
||
var h = '<div class="zcm-head"><img src="' + sub + '/zapretdiscordyoutube.png" alt=""><div><b>Zapret.Moe</b><small>git-сервер сообщества</small></div></div>';
|
||
h += '<div class="zcm-row"><button data-a="back" title="Назад"' + (history.length < 2 ? ' disabled' : '') + '>' + I.back + '</button><button data-a="fwd" title="Вперёд">' + I.fwd + '</button><button data-a="reload" title="Обновить">' + I.reload + '</button><button data-a="top" title="Наверх">' + I.top + '</button></div>';
|
||
var body = [];
|
||
if (sel) body.push('<div class="zcm-lbl">«' + sel.slice(0, 40).replace(/</g, '<') + (sel.length > 40 ? '…' : '') + '»</div>', it('copysel', I.copy, 'Копировать', 'Ctrl+C'), it('search', I.search, 'Найти в интернете'));
|
||
else if (a) body.push('<div class="zcm-lbl">' + a.href.replace(/^https?:\/\//, '').slice(0, 44) + '</div>', it('go', I.open, 'Открыть'), it('tab', I.open, 'Открыть в новой вкладке'), it('copylink', I.link, 'Копировать адрес ссылки'));
|
||
else if (im && im.currentSrc) body.push(it('imgopen', I.img, 'Открыть картинку'), it('imgcopy', I.link, 'Копировать адрес картинки'));
|
||
if (body.length) h += body.join('') + '<div class="zcm-sep"></div>';
|
||
h += it('intro', I.play, 'Посмотреть заставку') + it('lock', I.lock, 'Сломать замок РКН') + it('repos', I.repo, 'Все репозитории') + it('status', I.status, 'Статус сервисов');
|
||
h += '<div class="zcm-sep"></div>' + it('donate', I.heart, 'Поддержать проект', '', 'hot');
|
||
h += '<div class="zcm-foot">Shift + ПКМ — меню браузера</div>';
|
||
menu.innerHTML = h;
|
||
menu._ctx = { a: a, im: im, sel: sel };
|
||
items = Array.prototype.slice.call(menu.querySelectorAll('.zcm-it'));
|
||
}
|
||
function it(act, ico, label, kbd, cls) { return '<button class="zcm-it' + (cls ? ' ' + cls : '') + '" role="menuitem" data-a="' + act + '">' + ico + '<span>' + label + '</span>' + (kbd ? '<kbd>' + kbd + '</kbd>' : '') + '</button>' }
|
||
function act(name) {
|
||
var c = menu._ctx || {}; close();
|
||
var go = function (u, blank) { if (blank) open(u, '_blank', 'noopener'); else location.href = u };
|
||
switch (name) {
|
||
case 'back': history.back(); break;
|
||
case 'fwd': history.forward(); break;
|
||
case 'reload': location.reload(); break;
|
||
case 'top': scrollTo({ top: 0, behavior: 'smooth' }); break;
|
||
case 'copysel': copy(c.sel); break;
|
||
case 'search': go('https://duckduckgo.com/?q=' + encodeURIComponent(c.sel), 1); break;
|
||
case 'go': c.a.click(); break;
|
||
case 'tab': go(c.a.href, 1); break;
|
||
case 'copylink': copy(c.a.href, 'Ссылка скопирована'); break;
|
||
case 'imgopen': go(c.im.currentSrc, 1); break;
|
||
case 'imgcopy': copy(c.im.currentSrc, 'Адрес картинки скопирован'); break;
|
||
case 'intro': if (window.zpmIntro) { scrollTo(0, 0); window.zpmIntro.start('full') } break;
|
||
case 'lock': var l = document.getElementById('zlock'); if (l) l.scrollIntoView({ behavior: 'smooth', block: 'center' }); break;
|
||
case 'repos': go(sub + '/explore/repos'); break;
|
||
case 'status': go('https://status.zapret.moe/', 1); break;
|
||
case 'donate': go('https://t.me/zapretvpns_bot?start=site_ctxmenu', 1); break;
|
||
}
|
||
}
|
||
function focusIt(i) { items.forEach(function (x, k) { x.classList.toggle('on', k === i) }); cur = i; if (items[i]) items[i].focus({ preventScroll: true }) }
|
||
document.addEventListener('contextmenu', function (e) {
|
||
if (e.shiftKey || e.target.closest('input, textarea, select, [contenteditable=""], [contenteditable="true"], .zcm')) return;
|
||
if (document.documentElement.classList.contains('zpm-intro-on')) return;
|
||
e.preventDefault();
|
||
build(e);
|
||
menu.hidden = false;
|
||
var w = menu.offsetWidth, hh = menu.offsetHeight;
|
||
var x = Math.min(e.clientX, innerWidth - w - 8), y = Math.min(e.clientY, innerHeight - hh - 8);
|
||
menu.style.left = Math.max(8, x) + 'px'; menu.style.top = Math.max(8, y) + 'px';
|
||
menu.style.setProperty('--ox', (e.clientX - x) + 'px'); menu.style.setProperty('--oy', (e.clientY - y) + 'px');
|
||
menu.style.animation = 'none'; void menu.offsetWidth; menu.style.animation = '';
|
||
});
|
||
menu.addEventListener('click', function (e) { var b = e.target.closest('[data-a]'); if (b) act(b.getAttribute('data-a')) });
|
||
menu.addEventListener('mousemove', function (e) { var b = e.target.closest('.zcm-it'); if (b) focusIt(items.indexOf(b)) });
|
||
document.addEventListener('mousedown', function (e) { if (!menu.hidden && !menu.contains(e.target)) close() }, true);
|
||
addEventListener('scroll', function () { if (!menu.hidden) close() }, { passive: true });
|
||
addEventListener('blur', close); addEventListener('resize', close);
|
||
document.addEventListener('keydown', function (e) {
|
||
if (menu.hidden) return;
|
||
if (e.key === 'Escape') { e.preventDefault(); close() }
|
||
else if (e.key === 'ArrowDown') { e.preventDefault(); focusIt((cur + 1) % items.length) }
|
||
else if (e.key === 'ArrowUp') { e.preventDefault(); focusIt((cur - 1 + items.length) % items.length) }
|
||
else if (e.key === 'Enter' && items[cur]) { e.preventDefault(); act(items[cur].getAttribute('data-a')) }
|
||
});
|
||
})();
|
||
|
||
/* ── Курсор-пакет и анимация «блокировка → обход» по клику ─────────── */
|
||
(function () {
|
||
if (!window.matchMedia || !matchMedia('(pointer: fine)').matches || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||
var root = document.documentElement, body = document.body;
|
||
var dot = document.createElement('div'), ring = document.createElement('div'), cv = document.createElement('canvas');
|
||
dot.className = 'zcur-dot'; ring.className = 'zcur-ring'; cv.className = 'zcur-fx';
|
||
body.appendChild(cv); body.appendChild(ring); body.appendChild(dot);
|
||
root.classList.add('zcur-on', 'zcur-hide');
|
||
var g = cv.getContext('2d'), dpr = 1, W = 0, H = 0;
|
||
function size() { dpr = Math.min(2, devicePixelRatio || 1); W = innerWidth; H = innerHeight; cv.width = W * dpr; cv.height = H * dpr }
|
||
size(); addEventListener('resize', size);
|
||
var mx = -100, my = -100, rx = -100, ry = -100, trail = [], fx = [], raf = 0, idle = 0;
|
||
function state(t) {
|
||
var c = root.classList, el = t && t.closest ? t : null;
|
||
c.toggle('zcur-link', !!(el && el.closest('a, button, [role="button"], .zrn, .zc-dl, .zp-lg, .zlock-hit, .zg-canvas, summary, label')));
|
||
c.toggle('zcur-rkn', !!(el && el.closest('.zr-rkn, .zf-rkn, .zx-rig-rkn, .zx-rig-stamp, .zx-rig-defeat')));
|
||
c.toggle('zcur-love', !!(el && el.closest('.zj-help, .zlock, a[href*="zapretvpns_bot"]')));
|
||
c.toggle('zcur-text', !!(el && !el.closest('a, button') && el.closest('p, h1, h2, h3, li span, dd, code, .zt-t') && !el.closest('.zr-rkn')));
|
||
}
|
||
addEventListener('pointermove', function (e) {
|
||
if (e.pointerType !== 'mouse') return;
|
||
mx = e.clientX; my = e.clientY; root.classList.remove('zcur-hide');
|
||
trail.push({ x: mx, y: my, t: performance.now() });
|
||
state(e.target); kick();
|
||
}, { passive: true });
|
||
document.addEventListener('mouseleave', function () { root.classList.add('zcur-hide') });
|
||
addEventListener('pointerdown', function (e) {
|
||
if (e.pointerType !== 'mouse' || e.button !== 0) return;
|
||
root.classList.add('zcur-down');
|
||
var hot = e.target.closest && e.target.closest('a, button, [role="button"], .zrn, .zlock-hit, .zg-canvas, input, textarea, select');
|
||
if (root.classList.contains('zcur-love')) hearts(e.clientX, e.clientY);
|
||
else if (hot) ripple(e.clientX, e.clientY); else block(e.clientX, e.clientY);
|
||
kick();
|
||
});
|
||
addEventListener('pointerup', function () { root.classList.remove('zcur-down') });
|
||
function ripple(x, y) {
|
||
fx.push({ k: 'ring', x: x, y: y, t: 0, dur: 0.5, col: '79,209,255' });
|
||
for (var i = 0; i < 8; i++) { var a = i / 8 * 6.28; fx.push({ k: 'spark', x: x, y: y, vx: Math.cos(a) * 160, vy: Math.sin(a) * 160, t: 0, dur: 0.4, col: '191,240,255' }) }
|
||
}
|
||
function hearts(x, y) {
|
||
for (var i = 0; i < 10; i++) { var a = -Math.PI / 2 + (Math.random() - 0.5) * 2.2, v = 90 + Math.random() * 150; fx.push({ k: 'heart', x: x, y: y, vx: Math.cos(a) * v, vy: Math.sin(a) * v, t: 0, dur: 0.9 + Math.random() * 0.4, s: 6 + Math.random() * 7, h: [350, 335, 320][i % 3] }) }
|
||
}
|
||
// «блокировка»: красная рамка ТСПУ схлопывается — пакет раскалывается и уходит в обход
|
||
function block(x, y) { fx.push({ k: 'block', x: x, y: y, t: 0, dur: 0.75, ang: Math.random() * 1.2 - 0.6 }) }
|
||
function kick() { if (!raf) raf = requestAnimationFrame(frame) }
|
||
var last = 0;
|
||
function frame(now) {
|
||
raf = 0;
|
||
var dt = last ? Math.min(0.05, (now - last) / 1000) : 0.016; last = now;
|
||
rx += (mx - rx) * Math.min(1, dt * 18); ry += (my - ry) * Math.min(1, dt * 18);
|
||
dot.style.transform = 'translate(' + mx + 'px,' + my + 'px)';
|
||
ring.style.transform = 'translate(' + rx + 'px,' + ry + 'px)';
|
||
g.setTransform(dpr, 0, 0, dpr, 0, 0); g.clearRect(0, 0, W, H);
|
||
// шлейф-провод
|
||
while (trail.length && now - trail[0].t > 260) trail.shift();
|
||
var love = root.classList.contains('zcur-love');
|
||
if (trail.length > 1) {
|
||
g.lineCap = 'round'; g.lineJoin = 'round';
|
||
for (var i = 1; i < trail.length; i++) {
|
||
var p = trail[i - 1], q = trail[i], a = 1 - (now - q.t) / 260;
|
||
g.strokeStyle = love ? 'rgba(255,' + (107 + (1 - a) * 60 | 0) + ',131,' + (a * 0.6) + ')' : 'rgba(' + (79 + (1 - a) * 76 | 0) + ',' + (209 - (1 - a) * 86 | 0) + ',255,' + (a * 0.55) + ')';
|
||
g.lineWidth = 1 + a * 2.5; g.beginPath(); g.moveTo(p.x, p.y); g.lineTo(q.x, q.y); g.stroke();
|
||
}
|
||
}
|
||
fx.forEach(function (f) {
|
||
f.t += dt; var k = f.t / f.dur;
|
||
if (f.k === 'ring') {
|
||
g.strokeStyle = 'rgba(' + f.col + ',' + (1 - k) + ')'; g.lineWidth = 2;
|
||
g.beginPath(); g.arc(f.x, f.y, 8 + k * 40, 0, 6.28); g.stroke();
|
||
} else if (f.k === 'spark') {
|
||
f.x += f.vx * dt; f.y += f.vy * dt; f.vx *= 0.9; f.vy *= 0.9;
|
||
g.fillStyle = 'rgba(' + f.col + ',' + (1 - k) + ')'; g.fillRect(f.x - 1.5, f.y - 1.5, 3, 3);
|
||
} else if (f.k === 'heart') {
|
||
f.x += f.vx * dt; f.y += f.vy * dt; f.vy += 160 * dt; f.vx *= 0.97;
|
||
var s = f.s * (k < 0.15 ? k / 0.15 : 1);
|
||
g.save(); g.translate(f.x, f.y); g.rotate(f.vx * 0.002); g.globalAlpha = 1 - k * k;
|
||
g.fillStyle = 'hsl(' + f.h + ',90%,66%)';
|
||
g.beginPath(); g.moveTo(0, s * 0.9);
|
||
g.bezierCurveTo(-s * 1.4, 0, -s * 0.9, -s, 0, -s * 0.35);
|
||
g.bezierCurveTo(s * 0.9, -s, s * 1.4, 0, 0, s * 0.9); g.fill(); g.restore();
|
||
} else if (f.k === 'block') {
|
||
g.save(); g.translate(f.x, f.y);
|
||
var s, close = Math.min(1, k / 0.3);
|
||
if (k < 0.42) {
|
||
// рамка схлопывается вокруг пакета
|
||
s = 44 - close * 26;
|
||
g.rotate(f.ang * (1 - close));
|
||
g.strokeStyle = 'rgba(255,59,92,' + (0.4 + close * 0.6) + ')'; g.lineWidth = 2.5;
|
||
g.strokeRect(-s / 2, -s / 2, s, s);
|
||
g.fillStyle = 'rgba(255,107,131,' + close + ')'; g.font = '700 9px ui-monospace, monospace'; g.textAlign = 'center';
|
||
g.fillText('ТСПУ', 0, -s / 2 - 5);
|
||
g.fillStyle = '#5b7fff'; g.fillRect(-6, -4.5, 12, 9);
|
||
if (k > 0.3) { g.fillStyle = 'rgba(255,255,255,' + (1 - (k - 0.3) / 0.12) + ')'; g.beginPath(); g.arc(0, 0, 16, 0, 6.28); g.fill() }
|
||
} else {
|
||
// пакет раскалывается и уходит в обход, рамка ломается
|
||
var e = (k - 0.42) / 0.58, ease = 1 - Math.pow(1 - e, 3), d = ease * 46;
|
||
g.rotate(f.ang);
|
||
g.globalAlpha = 1 - e;
|
||
g.strokeStyle = '#ff3b5c'; g.lineWidth = 2.5;
|
||
[[-1, -1, 1, 0], [1, -1, 0, 1], [1, 1, -1, 0], [-1, 1, 0, -1]].forEach(function (c, j) {
|
||
g.save(); g.translate(c[0] * (9 + d * 0.8), c[1] * (9 + d * 0.8)); g.rotate((j % 2 ? 1 : -1) * ease * 1.2);
|
||
g.beginPath(); g.moveTo(-9, 0); g.lineTo(9, 0); g.stroke(); g.restore();
|
||
});
|
||
g.globalAlpha = 1 - e * 0.8;
|
||
g.fillStyle = '#5b7fff';
|
||
g.fillRect(-6 - d * 1.2, -4.5 - d * 0.5, 5.5, 9);
|
||
g.fillRect(0.5 + d * 1.2, -4.5 + d * 0.5, 5.5, 9);
|
||
g.fillStyle = 'rgba(79,209,255,' + (1 - e) + ')'; g.font = '700 10px ui-monospace, monospace'; g.textAlign = 'center';
|
||
g.fillText('обход ✓', 0, -20 - d * 0.4);
|
||
}
|
||
g.restore();
|
||
}
|
||
});
|
||
fx = fx.filter(function (f) { return f.t < f.dur });
|
||
if (Math.abs(mx - rx) + Math.abs(my - ry) > 0.3 || trail.length || fx.length) raf = requestAnimationFrame(frame);
|
||
else last = 0;
|
||
}
|
||
})();
|
||
|
||
/* ── На связи: программы, которые ждут сигнал о новой версии ─────────
|
||
Числа раз в минуту считает сервер (служба очереди обновлений) и кладёт
|
||
в stats.json. Нет файла или в нём пусто — раздела на странице нет.
|
||
Ряд приходит строками [время, dev, stable, на последней версии, выдано
|
||
разрешений, обновилось, не вышло]; кривая выпуска — парами [секунд от
|
||
выхода, доля в ‰]. Графики появляются плавно, когда раздел впервые попал
|
||
на экран и когда человек сам что-то переключил; при ежеминутном
|
||
обновлении чисел они не дёргаются. */
|
||
(function () {
|
||
var live = document.getElementById('zpm-live');
|
||
if (!live || !window.fetch) return;
|
||
var SVG = 'http://www.w3.org/2000/svg';
|
||
var CH = [['stable', 'Stable', 2], ['dev', 'Dev', 1]]; // ключ, подпись, столбец ряда
|
||
var S = null, first = true, range = '24h', on = { stable: true, dev: true }, picked = '';
|
||
var zoom = -1; // отрезок кривой выпуска, секунд; 0 — всё время, -1 — выбрать самому
|
||
var sorts = { days: ['d', -1], rolls: ['released_at', -1] };
|
||
var still = !!(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches);
|
||
var seen = false; // раздел уже побывал на экране
|
||
var LIST = 8; // выпусков в списке слева; в таблице — все
|
||
/* две программы — два итога сервера; подписи отличаются словами про своё */
|
||
var PRODUCTS = {
|
||
gui: { url: live.getAttribute('data-stats'), scope: 'Пока в счёте только Zapret 2 GUI начиная с версии 21.1.7.121 — число растёт по мере обновления.', run: 'обход включён' },
|
||
kvn: { url: live.getAttribute('data-stats-kvn'), scope: 'В счёте Zapret KVN для Windows начиная с выпуска, в котором появился сигнал сервера, — число растёт по мере обновления.', run: 'подключение включено' }
|
||
};
|
||
var product = 'gui', chosen = false; // chosen — человек сам переключил программу
|
||
|
||
function q(sel) { return live.querySelector(sel) }
|
||
function cell(key) { return q('[data-k="' + key + '"]') }
|
||
function show(key, visible) {
|
||
var box = cell(key);
|
||
while (box && !box.classList.contains('zp-kpi')) box = box.parentNode;
|
||
if (box) box.hidden = !visible;
|
||
}
|
||
function put(key, text) { var el = cell(key); if (el) el.textContent = text }
|
||
function two(n) { return ('0' + n).slice(-2) }
|
||
function clock(ts) { var d = new Date(ts * 1000); return two(d.getHours()) + ':' + two(d.getMinutes()) }
|
||
function day(ts) { var d = new Date(ts * 1000); return d.getDate() + ' ' + MONTHS[d.getMonth()] }
|
||
function span(s) {
|
||
if (s < 1) return 'сразу';
|
||
if (s < 90) return Math.round(s) + ' с';
|
||
if (s < 5400) return Math.round(s / 60) + ' мин';
|
||
if (s < 48 * 3600) return (s / 3600).toLocaleString('ru-RU', { maximumFractionDigits: 1 }) + ' ч';
|
||
return Math.round(s / 86400) + ' дн.';
|
||
}
|
||
/* «15–30 с», «2–5 мин»: единица пишется один раз, если она у границ общая */
|
||
function between(a, b) {
|
||
var u = b <= 60 ? [1, ' с'] : b <= 3600 && a >= 60 ? [60, ' мин'] : null;
|
||
return u ? a / u[0] + '–' + b / u[0] + u[1] : span(a) + ' – ' + span(b);
|
||
}
|
||
/* за сколько версия дошла до доли программ. У прежних выпусков время
|
||
мерилось минутами: «дошла за 0 минут» честно зовётся «меньше минуты» */
|
||
function reach(r, key) {
|
||
var s = r[key];
|
||
return s != null && s < 60 && !r.exact ? 'меньше минуты' : within(s);
|
||
}
|
||
function within(s) { return s == null ? 'ещё нет' : (s < 1 ? '' : 'за ') + span(s) }
|
||
function pct(share) { return (share * 100).toLocaleString('ru-RU', { maximumFractionDigits: share < 0.1 ? 1 : 0 }) + '%' }
|
||
function niceTop(v) {
|
||
if (v <= 10) return Math.max(2, Math.ceil(v / 2) * 2);
|
||
var mag = Math.pow(10, Math.floor(Math.log(v) / Math.LN10)) / 2;
|
||
return Math.ceil(v / mag) * mag;
|
||
}
|
||
|
||
/* Холст графика: размеры по месту, узлы SVG и подсказка у курсора. */
|
||
function stage(box, padL, animate) {
|
||
var svg = box.querySelector('svg'), tip = box.querySelector('.zp-tip');
|
||
/* класс снимается и ставится заново: так появление проигрывается ещё раз */
|
||
svg.classList.remove('zw-anim');
|
||
if (animate && !still) { void svg.getBoundingClientRect(); svg.classList.add('zw-anim') }
|
||
var w = Math.max(300, box.clientWidth || 900), h = Math.max(120, box.clientHeight || 180);
|
||
svg.textContent = '';
|
||
svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
|
||
tip.hidden = true;
|
||
var g = { svg: svg, tip: tip, w: w, h: h, l: padL, r: w - 6, t: 10, b: h - 24 };
|
||
/* подписи времени под графиком: ticks+1 штука от начала до конца */
|
||
g.times = function (t0, t1, x, label) {
|
||
var ticks = w < 560 ? 3 : 6;
|
||
for (var i = 0; i <= ticks; i++) {
|
||
var t = t0 + (t1 - t0) * i / ticks;
|
||
g.node('text', { 'class': 'zp-xl', x: Math.min(g.r - 20, Math.max(g.l + 18, x(t))), y: h - 4 }, label(t, i));
|
||
}
|
||
};
|
||
g.node = function (name, attrs, text, parent) {
|
||
var el = document.createElementNS(SVG, name);
|
||
for (var k in attrs) el.setAttribute(k, attrs[k]);
|
||
if (text != null) el.textContent = text;
|
||
(parent || svg).appendChild(el);
|
||
return el;
|
||
};
|
||
g.grid = function (top, label) {
|
||
[0, 0.5, 1].forEach(function (k) {
|
||
var y = g.b - (g.b - g.t) * k;
|
||
g.node('line', { 'class': 'zp-gl', x1: g.l, x2: g.r, y1: y, y2: y });
|
||
g.node('text', { 'class': 'zp-yl', x: g.l - 8, y: y + 4 }, label(top * k));
|
||
});
|
||
};
|
||
g.say = function (x, title, rows) {
|
||
fillTip(tip, title, rows);
|
||
var half = tip.offsetWidth / 2;
|
||
tip.style.left = Math.min(w - half - 2, Math.max(half + 2, x)) + 'px';
|
||
};
|
||
return g;
|
||
}
|
||
function color(key) { return getComputedStyle(live).getPropertyValue('--zw-' + key).trim() || '#5b7fff' }
|
||
|
||
/* подсказка у курсора: первая строка — заголовок, дальше [цвет|null, подпись, значение] */
|
||
function fillTip(tip, title, rows) {
|
||
tip.textContent = '';
|
||
var head = document.createElement('p'); head.textContent = title; tip.appendChild(head);
|
||
rows.forEach(function (r) {
|
||
var p = document.createElement('p');
|
||
if (r[0]) { var key = document.createElement('i'); key.style.background = r[0]; p.appendChild(key) }
|
||
var val = document.createElement('b'); val.textContent = r[2]; p.appendChild(val);
|
||
p.appendChild(document.createTextNode(r[1]));
|
||
tip.appendChild(p);
|
||
});
|
||
tip.hidden = false;
|
||
}
|
||
|
||
/* ── Живая сценка ────────────────────────────────────────────────────
|
||
Сервер в середине, программы вокруг него. У каждой — открытое
|
||
соединение (тонкая нить). Сервер держит вопрос «есть новее?» до
|
||
четырёх с половиной минут; срок вышел — отвечает «пока нет», и
|
||
программа спрашивает снова. Этих переспросов на экране столько,
|
||
сколько сервер насчитал за последнюю минуту. Когда выходит версия,
|
||
от сервера к программам идут разрешения обновиться. */
|
||
var scene = (function () {
|
||
var box = q('.zw-scene'), cv = box.querySelector('canvas'), log = box.querySelector('.zw-log');
|
||
var ctx = cv.getContext && cv.getContext('2d');
|
||
if (!ctx) return { update: function () {} };
|
||
var MAX_DOTS = 420, MAX_RATE = 7; // точек на холсте и пакетов в секунду — больше глаз не разберёт
|
||
var W = 0, H = 0, dpr = 1, dots = [], fly = [], rings = [], raf = 0, last = 0;
|
||
var rate = 0, askDebt = 0, grantsLeft = 0, grantDebt = 0, logAt = 0, visible = false, grantedSeen = null;
|
||
var info = { latest: '', channel: 'dev', old: '' };
|
||
var paint = {};
|
||
function tone(name, fallback) { return getComputedStyle(live).getPropertyValue(name).trim() || fallback }
|
||
function size() {
|
||
dpr = Math.min(2, window.devicePixelRatio || 1);
|
||
W = cv.clientWidth; H = cv.clientHeight;
|
||
cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr);
|
||
paint = { acc: tone('--acc', '#5b7fff'), old: tone('--zw-dev', '#c2803a'), gold: tone('--gold', '#e9bd55'), ink: tone('--ink', '#f2eee6'), mut: tone('--mut', '#a9a69f'), dim: tone('--dim', '#6f6d69'), mono: tone('--mono', 'monospace') };
|
||
place();
|
||
}
|
||
/* точки лежат «подсолнухом»: ровно заполняют овал и не слипаются */
|
||
function place() {
|
||
var n = dots.length, rx = W / 2 - 10, ry = H / 2 - 10, hole = Math.min(0.3, 54 / Math.max(1, Math.min(rx, ry)));
|
||
dots.forEach(function (d, i) {
|
||
var k = hole + (1 - hole) * Math.sqrt((i + 0.5) / n), a = i * 2.399963;
|
||
d.x = W / 2 + Math.cos(a) * rx * k; d.y = H / 2 + Math.sin(a) * ry * k;
|
||
});
|
||
}
|
||
function say(code, gloss, grant) {
|
||
var li = document.createElement('li');
|
||
if (grant) li.className = 'zw-log-grant';
|
||
var c = document.createElement('code'); c.textContent = code; li.appendChild(c);
|
||
li.appendChild(document.createTextNode(gloss));
|
||
log.insertBefore(li, log.firstChild);
|
||
while (log.children.length > 4) log.removeChild(log.lastChild);
|
||
}
|
||
function sayAsk(d, now) {
|
||
if (now - logAt < 1500) return;
|
||
logAt = now;
|
||
var known = d.old && info.old ? info.old : info.latest;
|
||
say('GET /api/wait?channel=' + info.channel + '&known=' + known + (d.busy ? '&busy=1' : ''),
|
||
d.busy ? 'программа: «я на ' + known + ', но сейчас занята — не обновляйте»' : 'программа: «я на ' + known + ', есть новее?» — сервер держит вопрос открытым');
|
||
}
|
||
function ask(now) {
|
||
var d = dots[Math.floor(Math.random() * dots.length)];
|
||
if (!d || d.wait) return;
|
||
/* срок ожидания вышел: сервер отвечает «пока нет», программа тут же спрашивает снова */
|
||
d.wait = true;
|
||
fly.push({ d: d, t: 0, dur: 0.7, back: true, kind: 'no', then: function () {
|
||
fly.push({ d: d, t: 0, dur: 0.8, back: false, kind: 'ask', then: function () { d.wait = false; rings.push({ t: 0, dur: 0.5, r: 12, gold: false }) } });
|
||
sayAsk(d, performance.now());
|
||
} });
|
||
}
|
||
function grant() {
|
||
var pool = dots.filter(function (d) { return d.old && !d.busy && !d.wait });
|
||
var d = pool[Math.floor(Math.random() * pool.length)];
|
||
if (!d) return;
|
||
d.wait = true;
|
||
rings.push({ t: 0, dur: 0.6, r: 12, gold: true });
|
||
say('{"version":"' + info.latest + '","changed":true}', 'сервер: «вышла ' + info.latest + ', можно обновляться»', true);
|
||
fly.push({ d: d, t: 0, dur: 0.9, back: true, kind: 'grant', then: function () {
|
||
d.glow = 1.6; // программа качает и ставит версию
|
||
setTimeout(function () {
|
||
d.old = false; d.wait = false;
|
||
fly.push({ d: d, t: 0, dur: 0.8, back: false, kind: 'ask', then: function () {} });
|
||
say('GET /api/wait?channel=' + info.channel + '&known=' + info.latest + '&prev=' + (info.old || '…') + '&took=24', 'программа: «обновилась, снова на связи» — по этим ответам сервер открывает версию следующим', true);
|
||
}, 1600);
|
||
} });
|
||
}
|
||
function draw() {
|
||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
ctx.clearRect(0, 0, W, H);
|
||
var cx = W / 2, cy = H / 2;
|
||
/* нити — открытые соединения: одна на программу */
|
||
ctx.beginPath();
|
||
dots.forEach(function (d) { ctx.moveTo(cx, cy); ctx.lineTo(d.x, d.y) });
|
||
ctx.strokeStyle = 'rgba(242,238,230,' + (dots.length > 200 ? 0.035 : 0.06) + ')'; ctx.lineWidth = 1; ctx.stroke();
|
||
dots.forEach(function (d) {
|
||
var c = d.old ? paint.old : paint.acc;
|
||
if (d.glow > 0) {
|
||
ctx.beginPath(); ctx.arc(d.x, d.y, 4 + (1.6 - d.glow) * 9, 0, 6.2832);
|
||
ctx.strokeStyle = paint.gold; ctx.globalAlpha = Math.max(0, d.glow / 1.6); ctx.lineWidth = 1.5; ctx.stroke(); ctx.globalAlpha = 1;
|
||
c = paint.gold;
|
||
}
|
||
ctx.beginPath(); ctx.arc(d.x, d.y, d.r, 0, 6.2832);
|
||
if (d.busy) { ctx.strokeStyle = paint.mut; ctx.lineWidth = 1.5; ctx.stroke() }
|
||
else { ctx.fillStyle = c; ctx.globalAlpha = d.tray && !(d.glow > 0) ? 0.4 : 1; ctx.fill(); ctx.globalAlpha = 1 }
|
||
});
|
||
/* пакеты: вопрос светлый, «пока нет» серый, разрешение золотое */
|
||
fly.forEach(function (p) {
|
||
var k = p.t / p.dur, e = k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2;
|
||
var u = p.back ? e : 1 - e;
|
||
var x = cx + (p.d.x - cx) * u, y = cy + (p.d.y - cy) * u;
|
||
ctx.save(); ctx.translate(x, y); ctx.rotate(Math.atan2(p.d.y - cy, p.d.x - cx));
|
||
ctx.fillStyle = p.kind === 'grant' ? paint.gold : p.kind === 'ask' ? '#bff0ff' : paint.dim;
|
||
var s = p.kind === 'grant' ? 1.3 : 1;
|
||
ctx.fillRect(-5 * s, -3 * s, 10 * s, 6 * s);
|
||
ctx.restore();
|
||
/* за пакетом светится его нить */
|
||
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(p.d.x, p.d.y);
|
||
ctx.strokeStyle = p.kind === 'grant' ? 'rgba(233,189,85,' + (0.5 * (1 - k)) + ')' : 'rgba(91,127,255,' + (0.35 * (1 - k)) + ')';
|
||
ctx.lineWidth = 1; ctx.stroke();
|
||
});
|
||
rings.forEach(function (r) {
|
||
var k = r.t / r.dur;
|
||
ctx.beginPath(); ctx.arc(cx, cy, r.r + k * 22, 0, 6.2832);
|
||
ctx.strokeStyle = r.gold ? paint.gold : paint.acc; ctx.globalAlpha = (1 - k) * 0.8; ctx.lineWidth = 1.5; ctx.stroke(); ctx.globalAlpha = 1;
|
||
});
|
||
/* сервер: квадрат-узел, на котором сходятся все нити */
|
||
ctx.fillStyle = tone('--bg', '#07080c'); ctx.fillRect(cx - 13, cy - 13, 26, 26);
|
||
ctx.strokeStyle = paint.ink; ctx.lineWidth = 2; ctx.strokeRect(cx - 12, cy - 12, 24, 24);
|
||
ctx.fillStyle = paint.acc; ctx.fillRect(cx - 5, cy - 5, 10, 10);
|
||
ctx.fillStyle = paint.mut; ctx.font = '11px ' + paint.mono; ctx.textAlign = 'center';
|
||
ctx.fillText('сервер', cx, cy + 30);
|
||
}
|
||
function frame(now) {
|
||
raf = 0;
|
||
var dt = last ? Math.min(0.05, (now - last) / 1000) : 0.016; last = now;
|
||
askDebt += rate * dt;
|
||
while (askDebt >= 1) { askDebt -= 1; ask(now) }
|
||
if (grantsLeft > 0) {
|
||
grantDebt += Math.max(0.6, grantsLeft / 20) * dt;
|
||
while (grantDebt >= 1 && grantsLeft > 0) { grantDebt -= 1; grantsLeft -= 1; grant() }
|
||
}
|
||
fly = fly.filter(function (p) { p.t += dt; if (p.t >= p.dur) { p.then(); return false } return true });
|
||
rings = rings.filter(function (r) { r.t += dt; return r.t < r.dur });
|
||
dots.forEach(function (d) { if (d.glow > 0) d.glow -= dt });
|
||
draw();
|
||
go();
|
||
}
|
||
function go() { if (!raf && visible && !document.hidden && !still && dots.length) raf = requestAnimationFrame(frame) }
|
||
function update(s) {
|
||
var total = s.online.total || 0;
|
||
box.hidden = !total;
|
||
if (box.hidden) return;
|
||
var per = Math.max(1, Math.ceil(total / MAX_DOTS)), n = Math.max(1, Math.round(total / per));
|
||
var latest = s.latest || {}, onLatest = 0, old = '';
|
||
(s.online.versions || []).forEach(function (v) {
|
||
if (v.channel && latest[v.channel] === v.version) onLatest += v.count;
|
||
else if (!old && v.version !== 'other') old = v.version;
|
||
});
|
||
info.channel = latest.dev && (s.online.dev || 0) >= (s.online.stable || 0) ? 'dev' : 'stable';
|
||
info.latest = latest[info.channel] || latest.dev || latest.stable || '';
|
||
info.old = old;
|
||
var oldDots = Math.round((total - onLatest) / per), busyDots = Math.min(n, Math.round((s.online.busy || 0) / per));
|
||
var act = s.online.activity || {}, told = 0;
|
||
for (var name in act) told += act[name] || 0;
|
||
var trayDots = told ? Math.round(n * (act.tray || 0) / told) : 0;
|
||
if (dots.length !== n) {
|
||
dots = [];
|
||
for (var i = 0; i < n; i++) dots.push({ x: 0, y: 0, r: n > 260 ? 2 : n > 120 ? 2.6 : 3.2, old: false, busy: false, tray: false, wait: false, glow: 0 });
|
||
}
|
||
/* кто на прежней версии и кто занят — раскиданы по полю, а не кучкой с краю */
|
||
var now = dots.filter(function (d) { return d.old }).length;
|
||
dots.forEach(function (d, i) {
|
||
if (now < oldDots && !d.old && (i * 7919) % n < oldDots * 2) { d.old = true; now++ }
|
||
else if (now > oldDots && d.old && !d.wait) { d.old = false; now-- }
|
||
d.busy = (i * 104729) % n < busyDots;
|
||
/* доля «в трее» — среди тех, кто о себе говорит; остальные рисуются как открытые */
|
||
d.tray = (i * 15485863) % n < trayDots;
|
||
});
|
||
put('scene-scale', per > 1 ? 'точка — ' + fmt(per) + ' ' + plural(per, 'программа', 'программы', 'программ') : 'точка — одна программа');
|
||
var rpm = s.requests_min != null ? s.requests_min : Math.round(total / 4.5);
|
||
put('rpm', fmt(rpm)); put('held', fmt(total));
|
||
var busyBox = cell('busy').parentNode; busyBox.hidden = !(s.online.busy > 0); put('busy', fmt(s.online.busy || 0));
|
||
rate = Math.min(MAX_RATE, rpm / 60);
|
||
/* разрешения, выданные с прошлого обновления чисел, разыгрываются на сценке */
|
||
var granted = 0;
|
||
for (var key in (s.rollout || {})) granted += s.rollout[key].granted || 0;
|
||
if (grantedSeen != null && granted > grantedSeen) grantsLeft += Math.min(120, Math.ceil((granted - grantedSeen) / per));
|
||
grantedSeen = granted;
|
||
if (!W || cv.clientWidth !== W) size(); else place();
|
||
if (!log.children.length && info.latest) {
|
||
say('{"version":"' + info.latest + '","changed":false}', 'сервер: «новее ' + info.latest + ' пока нет — спросите снова»');
|
||
say('GET /api/wait?channel=' + info.channel + '&known=' + info.latest, 'программа: «я на ' + info.latest + ', есть новее?» — сервер держит вопрос открытым');
|
||
}
|
||
draw();
|
||
go();
|
||
}
|
||
if (window.IntersectionObserver) {
|
||
new IntersectionObserver(function (es) {
|
||
visible = es.some(function (e) { return e.isIntersecting });
|
||
last = 0; go();
|
||
}, { rootMargin: '80px 0px' }).observe(cv);
|
||
} else visible = true;
|
||
document.addEventListener('visibilitychange', function () { last = 0; go() });
|
||
window.addEventListener('resize', function () { if (dots.length) { size(); draw() } });
|
||
function reset() { dots = []; fly = []; rings = []; grantsLeft = 0; grantedSeen = null; log.textContent = '' }
|
||
return { update: update, reset: reset };
|
||
})();
|
||
|
||
/* чем заняты программы — по их собственным словам; считается общим числом */
|
||
var ACT = [
|
||
['window', 'окно открыто', 'zw-now'],
|
||
['tray', 'в трее, работают в фоне', ''],
|
||
['fullscreen', 'игра или видео на весь экран', 'zw-busy'],
|
||
['blockcheck', 'проверяют сеть', 'zw-busy'],
|
||
['strategy_scan', 'подбирают стратегию', 'zw-busy']
|
||
];
|
||
function drawActivity() {
|
||
var box = q('.zw-act'), bar = box.querySelector('.zw-bar'), list = box.querySelector('.zw-list');
|
||
var a = S.online.activity || {}, total = 0;
|
||
ACT.forEach(function (c) { total += a[c[0]] || 0 });
|
||
box.hidden = !total;
|
||
if (box.hidden) return;
|
||
bar.textContent = ''; list.textContent = '';
|
||
ACT.forEach(function (c) {
|
||
var n = a[c[0]] || 0;
|
||
if (!n) return;
|
||
var seg = document.createElement('i');
|
||
seg.style.flexGrow = String(n);
|
||
seg.className = c[2];
|
||
seg.setAttribute('role', 'img');
|
||
seg.setAttribute('aria-label', c[1] + ': ' + n + ', ' + pct(n / total));
|
||
bar.appendChild(seg);
|
||
var li = document.createElement('li');
|
||
li.className = c[2];
|
||
var name = document.createElement('b'); name.textContent = c[1]; li.appendChild(name);
|
||
li.appendChild(document.createTextNode(' ' + fmt(n) + ' · ' + pct(n / total)));
|
||
list.appendChild(li);
|
||
});
|
||
var run = S.online.running || [0, 0], notes = [];
|
||
if (run[1]) notes.push(PRODUCTS[product].run + ' у ' + fmt(run[0]) + ' из ' + fmt(run[1]) + ' (' + pct(run[0] / run[1]) + ')');
|
||
if (total < (S.online.total || 0)) notes.push('о себе говорят ' + fmt(total) + ' из ' + fmt(S.online.total) + ' — остальные на прежних версиях программы');
|
||
put('act-note', notes.join(' · '));
|
||
}
|
||
|
||
/* хвост раздачи: почему программы на связи ещё на прежней версии */
|
||
var TAIL = [
|
||
['busy', 'играют или смотрят видео — обновятся, когда человек закончит', 'zw-busy'],
|
||
['stage', 'ещё не их ступень раздачи', 'zw-now'],
|
||
['queued', 'ждут очереди на скачивание', ''],
|
||
['other', 'прямо сейчас качают и ставят или только что запустились', '']
|
||
];
|
||
function drawTail() {
|
||
var box = q('.zw-tail'), bar = box.querySelector('.zw-bar'), list = box.querySelector('.zw-list');
|
||
var parts = {}, old = 0;
|
||
Object.keys(S.tail || {}).forEach(function (ch) {
|
||
var t = S.tail[ch] || {};
|
||
old += t.old || 0;
|
||
TAIL.forEach(function (c) { parts[c[0]] = (parts[c[0]] || 0) + (t[c[0]] || 0) });
|
||
});
|
||
box.hidden = !old;
|
||
if (box.hidden) return;
|
||
bar.textContent = ''; list.textContent = '';
|
||
TAIL.forEach(function (c) {
|
||
var n = parts[c[0]] || 0;
|
||
if (!n) return;
|
||
var seg = document.createElement('i');
|
||
seg.style.flexGrow = String(n);
|
||
seg.className = c[2];
|
||
seg.setAttribute('role', 'img');
|
||
seg.setAttribute('aria-label', c[1] + ': ' + n + ', ' + pct(n / old));
|
||
bar.appendChild(seg);
|
||
var li = document.createElement('li');
|
||
li.className = c[2];
|
||
var name = document.createElement('b'); name.textContent = c[1]; li.appendChild(name);
|
||
li.appendChild(document.createTextNode(' ' + fmt(n) + ' · ' + pct(n / old)));
|
||
list.appendChild(li);
|
||
});
|
||
put('tail-note', fmt(old) + ' из ' + fmt(S.online.total || old) + ' на связи');
|
||
}
|
||
|
||
/* коды причин, которые присылают программы (why — не поставилось, miss — не скачалось) */
|
||
var WHY = {
|
||
gui: {
|
||
exit1: 'установщик не смог начать', exit2: 'установку отменили до начала', exit3: 'установщик сломался между шагами',
|
||
exit4: 'установщик сломался посреди установки', exit5: 'установщик прервал установку — обычно файл занят',
|
||
exit6: 'установщик закрыли принудительно', exit7: 'установщик отказался ставить', exit8: 'установщик просит перезапустить Windows',
|
||
same: 'установщик отчитался, а версия на диске прежняя', tamper: 'файл установщика изменили после проверки',
|
||
gone: 'файл установщика пропал — часто антивирус', start: 'установщик не запустился — антивирус или права',
|
||
cut: 'установка оборвалась — перезагрузка или выключение'
|
||
},
|
||
kvn: {
|
||
crash: 'новая версия падала при запуске — вернули прежнюю', lock: 'файл программы держит другой процесс',
|
||
swap: 'не вышло заменить файлы программы', prep: 'не вышло подготовить файлы — место или права',
|
||
close: 'программа не закрылась перед заменой'
|
||
},
|
||
all: {
|
||
net: 'сеть: файл не скачался', hash: 'файл не совпал с контрольной суммой', disk: 'нет места на диске или нельзя записать',
|
||
meta: 'не получили данные выпуска', handoff: 'не смогли передать файл установщику', badzip: 'архив повреждён',
|
||
bad: 'архив отклонён', old: 'прежние версии программы причину не называют', other: 'причина не распознана'
|
||
}
|
||
};
|
||
function whyText(code) {
|
||
var own = WHY[product] || {};
|
||
if (own[code]) return own[code];
|
||
if (WHY.all[code]) return WHY.all[code];
|
||
var m = /^exitx([0-9a-f]+)$/.exec(code);
|
||
if (m) return 'установщик аварийно завершился (0x' + m[1].toUpperCase() + ')';
|
||
m = /^exit(\d+)$/.exec(code);
|
||
if (m) return 'установщик вернул код ' + m[1];
|
||
return code;
|
||
}
|
||
function drawWhy() {
|
||
var box = q('.zw-why'), u = S.updates_24h || {}, any = false;
|
||
['why', 'miss'].forEach(function (kind) {
|
||
var col = box.querySelector('[data-w="' + kind + '"]'), list = col.querySelector('.zw-list');
|
||
var codes = u[kind] || {}, keys = Object.keys(codes).filter(function (k) { return codes[k] > 0 });
|
||
keys.sort(function (a, b) { return codes[b] - codes[a] || (a < b ? -1 : 1) });
|
||
list.textContent = '';
|
||
col.hidden = !keys.length;
|
||
if (!keys.length) return;
|
||
any = true;
|
||
keys.slice(0, 8).forEach(function (k) {
|
||
var li = document.createElement('li');
|
||
var name = document.createElement('b'); name.textContent = fmt(codes[k]); li.appendChild(name);
|
||
li.appendChild(document.createTextNode(' ' + whyText(k)));
|
||
li.title = k;
|
||
list.appendChild(li);
|
||
});
|
||
});
|
||
box.hidden = !any;
|
||
}
|
||
|
||
function drawVersions() {
|
||
var wire = q('.zw-wire'), bar = wire.querySelector('.zw-bar'), list = wire.querySelector('.zw-list'), tip = wire.querySelector('.zp-tip');
|
||
var items = (S.online.versions || []).filter(function (v) { return v.count > 0 });
|
||
var total = S.online.total || 1;
|
||
wire.hidden = !items.length;
|
||
bar.textContent = ''; list.textContent = '';
|
||
tip.hidden = true;
|
||
bar.classList.remove('zw-hov');
|
||
var pairs = [];
|
||
function tell(i) {
|
||
var v = items[i], seg = pairs[i][0];
|
||
pairs.forEach(function (p, k) { p[0].classList.toggle('zw-hot', k === i); p[1].classList.toggle('zw-hot', k === i) });
|
||
bar.classList.add('zw-hov');
|
||
var rows = [[null, ' ' + plural(v.count, 'программа', 'программы', 'программ') + ' на связи', fmt(v.count)], [null, ' от всех на связи', pct(v.count / total)]];
|
||
if (v.users) rows.push([null, ' ' + plural(v.users, 'пользователь', 'пользователя', 'пользователей') + ' за сегодня', fmt(v.users)]);
|
||
var last = !!v.channel && S.latest && S.latest[v.channel] === v.version;
|
||
fillTip(tip, (v.version === 'other' ? 'остальные версии' : v.version + (v.channel ? ' · ' + v.channel : '')) + (last ? ' · последняя' : ''), rows);
|
||
var half = tip.offsetWidth / 2, mid = seg.offsetLeft + seg.offsetWidth / 2;
|
||
tip.style.left = Math.min(wire.clientWidth - half, Math.max(half, mid)) + 'px';
|
||
/* над полосой: подсказка не закрывает ни отрезок под курсором, ни строки версий */
|
||
tip.style.top = (bar.offsetTop - tip.offsetHeight - 12) + 'px';
|
||
}
|
||
function hush() {
|
||
pairs.forEach(function (p) { p[0].classList.remove('zw-hot'); p[1].classList.remove('zw-hot') });
|
||
bar.classList.remove('zw-hov');
|
||
tip.hidden = true;
|
||
}
|
||
items.forEach(function (v, i) {
|
||
var now = !!v.channel && S.latest && S.latest[v.channel] === v.version;
|
||
var seg = document.createElement('i');
|
||
seg.style.flexGrow = String(v.count);
|
||
seg.tabIndex = 0;
|
||
seg.setAttribute('role', 'img');
|
||
seg.setAttribute('aria-label', v.version + ': ' + v.count + ', ' + pct(v.count / total));
|
||
if (now) seg.className = 'zw-now';
|
||
bar.appendChild(seg);
|
||
var li = document.createElement('li');
|
||
li.tabIndex = 0;
|
||
if (now) li.className = 'zw-now';
|
||
var name = document.createElement('b');
|
||
name.textContent = v.version === 'other' ? 'остальные' : v.version;
|
||
li.appendChild(name);
|
||
/* всё, что есть в подсказке, написано и в строке: наведение ничего не прячет */
|
||
li.appendChild(document.createTextNode(' ' + fmt(v.count) + ' · ' + pct(v.count / total) + (v.channel ? ' · ' + v.channel : '')));
|
||
list.appendChild(li);
|
||
pairs.push([seg, li]);
|
||
[seg, li].forEach(function (el) {
|
||
el.addEventListener('pointerenter', function () { tell(i) });
|
||
el.addEventListener('focus', function () { tell(i) });
|
||
el.addEventListener('pointerleave', hush);
|
||
el.addEventListener('blur', hush);
|
||
});
|
||
});
|
||
}
|
||
|
||
function drawOnline(animate) {
|
||
var box = q('.zw-day'), rows = ((S.series || {})[range]) || [];
|
||
var keys = CH.filter(function (c) { return on[c[0]] });
|
||
box.hidden = rows.length < 2;
|
||
q('.zw-ctl').hidden = ((S.series || {})['24h'] || []).length < 2;
|
||
if (box.hidden) return;
|
||
var g = stage(box.querySelector('.zw-chart'), 48, animate);
|
||
var t0 = rows[0][0], t1 = rows[rows.length - 1][0];
|
||
function sum(r) { var s = 0; keys.forEach(function (c) { s += r[c[2]] }); return s }
|
||
var top = niceTop(Math.max.apply(null, rows.map(sum)) || 1);
|
||
function x(t) { return g.l + (t - t0) / Math.max(1, t1 - t0) * (g.r - g.l) }
|
||
function y(v) { return g.b - v / top * (g.b - g.t) }
|
||
g.grid(top, short);
|
||
/* каналы лежат друг на друге: верхняя граница — все программы вместе */
|
||
var base = rows.map(function () { return 0 });
|
||
keys.forEach(function (c) {
|
||
var up = rows.map(function (r, i) { return base[i] + r[c[2]] });
|
||
var line = rows.map(function (r, i) { return x(r[0]).toFixed(1) + ',' + y(up[i]).toFixed(1) });
|
||
var back = rows.map(function (r, i) { return x(r[0]).toFixed(1) + ',' + y(base[i]).toFixed(1) }).reverse();
|
||
g.node('polygon', { 'class': 'zw-fade zw-a-' + c[0], points: line.concat(back).join(' ') });
|
||
g.node('polyline', { 'class': 'zw-draw zw-l-' + c[0], pathLength: 1, points: line.join(' ') });
|
||
base = up;
|
||
});
|
||
g.times(t0, t1, x, function (t) { return range === '24h' ? clock(t) : day(t) });
|
||
var cur = g.node('line', { 'class': 'zp-cur', y1: g.t, y2: g.b, visibility: 'hidden' });
|
||
var hit = g.node('rect', { 'class': 'zw-hit', x: g.l, y: 0, width: g.r - g.l, height: g.h });
|
||
function at(ev) {
|
||
var rect = g.svg.getBoundingClientRect();
|
||
var px = (ev.clientX - rect.left) / rect.width * g.w;
|
||
var best = 0;
|
||
rows.forEach(function (r, i) { if (Math.abs(x(r[0]) - px) < Math.abs(x(rows[best][0]) - px)) best = i });
|
||
var r = rows[best], total = sum(r), all = r[1] + r[2];
|
||
cur.setAttribute('x1', x(r[0])); cur.setAttribute('x2', x(r[0])); cur.setAttribute('visibility', 'visible');
|
||
var lines = [[null, ' всего на связи', fmt(total)]];
|
||
keys.forEach(function (c) { lines.push([color(c[0]), ' ' + c[1], fmt(r[c[2]])]) });
|
||
if (all) lines.push([null, ' на последней версии', pct(r[3] / all)]);
|
||
g.say(x(r[0]) / g.w * rect.width, day(r[0]) + ', ' + clock(r[0]), lines);
|
||
}
|
||
hit.addEventListener('pointermove', at);
|
||
hit.addEventListener('pointerdown', at);
|
||
hit.addEventListener('pointerleave', function () { cur.setAttribute('visibility', 'hidden'); g.tip.hidden = true });
|
||
}
|
||
|
||
/* обновления: контур — сколько программ получили разрешение, заливка —
|
||
сколько из них обновились и вышли на связь, красное — не удалось */
|
||
function drawUpdates(animate) {
|
||
var box = q('.zw-upd'), rows = ((S.series || {})[range]) || [];
|
||
var any = rows.some(function (r) { return (r[4] || 0) + (r[5] || 0) + (r[6] || 0) > 0 });
|
||
box.hidden = !any || rows.length < 2;
|
||
if (box.hidden) return;
|
||
var g = stage(box.querySelector('.zw-chart'), 48, animate);
|
||
var top = niceTop(Math.max.apply(null, rows.map(function (r) { return Math.max(r[4] || 0, (r[5] || 0) + (r[6] || 0)) })) || 1);
|
||
var slot = (g.r - g.l) / rows.length, bw = Math.max(2, Math.min(18, slot - 2));
|
||
function cx(i) { return g.l + slot * (i + 0.5) }
|
||
function y(v) { return g.b - v / top * (g.b - g.t) }
|
||
g.grid(top, short);
|
||
rows.forEach(function (r, i) {
|
||
var grant = r[4] || 0, done = r[5] || 0, fail = r[6] || 0;
|
||
if (!(grant + done + fail)) return;
|
||
var col = g.node('g', { 'class': 'zw-bcol zw-grow', style: '--i:' + i });
|
||
if (grant) g.node('rect', { 'class': 'zw-b-grant', x: cx(i) - bw / 2 + 0.5, width: Math.max(1, bw - 1), y: y(grant), height: Math.max(2, g.b - y(grant)) }, null, col);
|
||
if (done) g.node('rect', { 'class': 'zw-b-done', x: cx(i) - bw / 2, width: bw, y: Math.min(y(done), g.b - 2), height: Math.max(2, g.b - y(done)) }, null, col);
|
||
if (fail) g.node('rect', { 'class': 'zw-b-fail', x: cx(i) - bw / 2, width: bw, y: Math.min(y(done + fail), y(done) - 3), height: Math.max(3, y(done) - y(done + fail) - (done ? 2 : 0)) }, null, col);
|
||
});
|
||
g.times(0, rows.length - 1, function (i) { return cx(i) }, function (i) { var t = rows[Math.round(i)][0]; return range === '24h' ? clock(t) : day(t) });
|
||
var cur = g.node('line', { 'class': 'zp-cur', y1: g.t, y2: g.b, visibility: 'hidden' });
|
||
var hit = g.node('rect', { 'class': 'zw-hit', x: g.l, y: 0, width: g.r - g.l, height: g.h });
|
||
function at(ev) {
|
||
var rect = g.svg.getBoundingClientRect(), px = (ev.clientX - rect.left) / rect.width * g.w;
|
||
var i = Math.max(0, Math.min(rows.length - 1, Math.floor((px - g.l) / slot))), r = rows[i];
|
||
cur.setAttribute('x1', cx(i)); cur.setAttribute('x2', cx(i)); cur.setAttribute('visibility', 'visible');
|
||
g.say(cx(i) / g.w * rect.width, day(r[0]) + ', ' + clock(r[0]), [
|
||
[null, ' ' + plural(r[4] || 0, 'программа получила', 'программы получили', 'программ получили') + ' разрешение', fmt(r[4] || 0)],
|
||
[color('done'), ' обновились и вышли на связь', fmt(r[5] || 0)],
|
||
[color('fail'), ' обновиться не удалось', fmt(r[6] || 0)]
|
||
]);
|
||
}
|
||
hit.addEventListener('pointermove', at);
|
||
hit.addEventListener('pointerdown', at);
|
||
hit.addEventListener('pointerleave', function () { cur.setAttribute('visibility', 'hidden'); g.tip.hidden = true });
|
||
}
|
||
|
||
/* Таблицы: сортировка по щелчку на заголовке, полоска в ячейке — доля от
|
||
наибольшего. cells(item) -> [[текст, доля|null, класс|''], …] */
|
||
function compare(a, b) {
|
||
if (a == null || a === '') return 1;
|
||
if (b == null || b === '') return -1;
|
||
if (typeof a === 'string' && /^\d+(\.\d+)+$/.test(a) && typeof b === 'string') {
|
||
var x = a.split('.'), y = b.split('.');
|
||
for (var i = 0; i < Math.max(x.length, y.length); i++) { var d = (+x[i] || 0) - (+y[i] || 0); if (d) return d }
|
||
return 0;
|
||
}
|
||
return a < b ? -1 : a > b ? 1 : 0;
|
||
}
|
||
function fillTable(table, items, cells) {
|
||
var sort = sorts[table.getAttribute('data-t')], body = table.querySelector('tbody');
|
||
var rows = items.slice().sort(function (a, b) {
|
||
/* пустые значения всегда внизу, в какую сторону ни сортируй */
|
||
var x = a[sort[0]], y = b[sort[0]];
|
||
if (x == null || y == null) return compare(x, y);
|
||
return compare(x, y) * sort[1];
|
||
});
|
||
body.textContent = '';
|
||
rows.forEach(function (item) {
|
||
var tr = document.createElement('tr');
|
||
if (item.halted) tr.className = 'zw-row-halt';
|
||
cells(item).forEach(function (c) {
|
||
var td = document.createElement('td');
|
||
td.textContent = c[0];
|
||
if (c[1] != null) { td.className = 'zw-barc'; td.style.setProperty('--v', Math.max(0, Math.min(1, c[1])).toFixed(3)) }
|
||
if (c[2]) td.classList.add(c[2]);
|
||
tr.appendChild(td);
|
||
});
|
||
body.appendChild(tr);
|
||
});
|
||
table.querySelectorAll('th').forEach(function (th) {
|
||
var mine = th.getAttribute('data-s') === sort[0];
|
||
th.setAttribute('aria-sort', mine ? (sort[1] > 0 ? 'ascending' : 'descending') : 'none');
|
||
});
|
||
}
|
||
function drawDayTable() {
|
||
var days = S.days || [], most = Math.max.apply(null, days.map(function (d) { return d.total })) || 1;
|
||
fillTable(q('table[data-t="days"]'), days, function (d) {
|
||
return [[d.d.slice(8) + '.' + d.d.slice(5, 7), null, ''], [fmt(d.total), d.total / most, ''], [fmt(d.stable), null, ''], [fmt(d.dev), null, '']];
|
||
});
|
||
}
|
||
function drawHistory() {
|
||
var box = q('.zw-hist'), rolls = S.rollouts || [];
|
||
box.hidden = !rolls.length;
|
||
if (box.hidden) return;
|
||
fillTable(q('table[data-t="rolls"]'), rolls, function (r) {
|
||
var staged = (r.stages || []).length > 0;
|
||
var open = r.halted ? 'остановлена на ' + r.percent + '%' : (r.full_sec != null ? within(r.full_sec) : (staged ? 'идёт, ' + r.percent + '%' : '—'));
|
||
var got = r.granted || 0, came = r.confirmed || 0;
|
||
return [
|
||
[r.version + ' · ' + r.channel, null, 'zw-td-name'],
|
||
[day(r.released_at) + ', ' + clock(r.released_at), null, ''],
|
||
[reach(r, 'half_sec'), null, ''],
|
||
[reach(r, 'ninety_sec'), null, ''],
|
||
[open, null, r.halted ? 'zw-td-bad' : ''],
|
||
[got ? fmt(got) : '—', null, ''],
|
||
[got ? fmt(came) + ' · ' + pct(Math.min(1, came / got)) : (came ? fmt(came) : '—'), got ? came / got : null, ''],
|
||
[r.failed ? fmt(r.failed) : '—', null, r.failed ? 'zw-td-bad' : ''],
|
||
[r.took_median != null ? span(r.took_median) : '—', null, '']
|
||
];
|
||
});
|
||
}
|
||
|
||
function drawDays(animate) {
|
||
var box = q('.zw-days'), days = S.days || [];
|
||
var keys = CH.filter(function (c) { return on[c[0]] });
|
||
box.hidden = days.length < 2;
|
||
if (box.hidden) return;
|
||
drawDayTable();
|
||
var g = stage(box.querySelector('.zw-chart'), 48, animate);
|
||
function sum(d) { var s = 0; keys.forEach(function (c) { s += d[c[0]] }); return s }
|
||
var top = niceTop(Math.max.apply(null, days.map(sum)) || 1);
|
||
var slot = (g.r - g.l) / days.length, bw = Math.max(3, Math.min(34, slot - 2));
|
||
function y(v) { return g.b - v / top * (g.b - g.t) }
|
||
g.grid(top, short);
|
||
var every = Math.ceil(days.length / (g.w < 560 ? 4 : 10));
|
||
days.forEach(function (d, i) {
|
||
var cx = g.l + slot * (i + 0.5), col = g.node('g', { 'class': 'zw-bcol zw-grow', style: '--i:' + i }), base = 0;
|
||
keys.forEach(function (c) {
|
||
var v = d[c[0]];
|
||
if (v > 0) g.node('rect', { 'class': 'zw-b-' + c[0], x: cx - bw / 2, width: bw, y: y(base + v), height: Math.max(1, y(base) - y(base + v) - (base ? 2 : 0)) }, null, col);
|
||
base += v;
|
||
});
|
||
if (i % every === 0 || i === days.length - 1) g.node('text', { 'class': 'zp-xl', x: cx, y: g.h - 4 }, d.d.slice(8) + '.' + d.d.slice(5, 7));
|
||
var hit = g.node('rect', { 'class': 'zw-hit', x: cx - slot / 2, width: slot, y: 0, height: g.h, tabindex: 0 }, null, col);
|
||
function tell() {
|
||
col.classList.add('zw-hot');
|
||
var lines = [[null, ' ' + plural(d.total, 'пользователь', 'пользователя', 'пользователей') + ' всего', fmt(d.total)]];
|
||
keys.forEach(function (c) { lines.push([color(c[0]), ' ' + c[1], fmt(d[c[0]])]) });
|
||
g.say(cx / g.w * g.svg.getBoundingClientRect().width, d.d.slice(8) + ' ' + MONTHS[+d.d.slice(5, 7) - 1], lines);
|
||
}
|
||
function hush() { col.classList.remove('zw-hot'); g.tip.hidden = true }
|
||
hit.addEventListener('pointerenter', tell); hit.addEventListener('focus', tell);
|
||
hit.addEventListener('pointerleave', hush); hit.addEventListener('blur', hush);
|
||
});
|
||
}
|
||
|
||
function drawRollout(keepList, animate) {
|
||
var box = q('.zw-roll'), rolls = (S.rollouts || []).filter(function (r) { return (r.curve || []).length > 1 }).slice(0, LIST);
|
||
box.hidden = !rolls.length;
|
||
if (box.hidden) return;
|
||
var list = box.querySelector('.zw-rels');
|
||
function id(r) { return r.channel + '|' + r.version }
|
||
if (!rolls.some(function (r) { return id(r) === picked })) { picked = id(rolls[0]); zoom = -1 }
|
||
/* выпуск выбирается наведением. Список при этом не перестраивается:
|
||
иначе строка под курсором исчезала бы и наведение срывалось. */
|
||
if (!keepList) {
|
||
list.textContent = '';
|
||
rolls.forEach(function (r) {
|
||
var b = document.createElement('button');
|
||
b.type = 'button'; b.className = 'zw-rel' + (r.halted ? ' zw-rel-halt' : '');
|
||
b.setAttribute('data-id', id(r));
|
||
var name = document.createElement('b'); name.textContent = r.version + ' · ' + r.channel; b.appendChild(name);
|
||
var when = document.createElement('span'); when.textContent = 'вышла ' + day(r.released_at) + ' в ' + clock(r.released_at); b.appendChild(when);
|
||
var speed = document.createElement('span');
|
||
speed.textContent = r.halted ? 'раздача остановлена на ' + r.percent + '%' : 'половина — ' + reach(r, 'half_sec') + ', 90% — ' + reach(r, 'ninety_sec');
|
||
b.appendChild(speed);
|
||
function pick() { if (picked !== id(r)) { picked = id(r); zoom = -1; drawRollout(true, true) } }
|
||
b.addEventListener('pointerenter', pick);
|
||
b.addEventListener('focus', pick);
|
||
b.addEventListener('click', pick);
|
||
list.appendChild(b);
|
||
});
|
||
}
|
||
list.querySelectorAll('.zw-rel').forEach(function (b) { b.setAttribute('aria-pressed', b.getAttribute('data-id') === picked ? 'true' : 'false') });
|
||
var roll = rolls.filter(function (r) { return id(r) === picked })[0];
|
||
var all = roll.curve, end = all[all.length - 1][0];
|
||
/* отрезок по умолчанию — тот, где версия успела разойтись: у быстрого
|
||
выпуска первые минуты не сжимаются в точку на шкале в сутки */
|
||
if (zoom < 0) zoom = roll.ninety_sec != null && roll.ninety_sec < 400 && end > 600 ? 600 : (roll.ninety_sec != null && roll.ninety_sec < 2400 && end > 3600 ? 3600 : 0);
|
||
box.querySelectorAll('.zw-zoom button').forEach(function (b) {
|
||
var z = +b.getAttribute('data-z');
|
||
b.hidden = z > 0 && end <= z;
|
||
b.setAttribute('aria-pressed', z === zoom ? 'true' : 'false');
|
||
});
|
||
var pts = zoom ? all.filter(function (p) { return p[0] <= zoom }) : all;
|
||
if (pts.length < 2) pts = all;
|
||
q('.zw-roll-cap').textContent = 'доля программ канала ' + roll.channel + ' на версии ' + roll.version + ' после выхода';
|
||
var g = stage(box.querySelector('.zw-curve'), 48, animate);
|
||
var m1 = Math.max(60, zoom || pts[pts.length - 1][0]);
|
||
function x(m) { return g.l + m / m1 * (g.r - g.l) }
|
||
function y(v) { return g.b - v / 1000 * (g.b - g.t) }
|
||
g.grid(100, function (v) { return Math.round(v) + '%' });
|
||
g.node('line', { 'class': 'zw-mark', x1: g.l, x2: g.r, y1: y(900), y2: y(900) });
|
||
g.node('text', { 'class': 'zw-mark-l', x: g.r, y: y(900) - 5, 'text-anchor': 'end' }, '90%');
|
||
/* ступени раздачи: с этого мига версию могла получить такая доля программ */
|
||
var marks = (roll.stages || []).filter(function (st) { return st[0] <= m1 });
|
||
marks.forEach(function (st, k) {
|
||
var sx = x(st[0]), next = marks[k + 1];
|
||
g.node('line', { 'class': 'zw-stage-l zw-fade', x1: sx, x2: sx, y1: g.t, y2: g.b });
|
||
/* ступени, слипшиеся на шкале, подписывает последняя из них */
|
||
if (next && x(next[0]) - sx < 34) return;
|
||
g.node('text', { 'class': 'zw-stage-t zw-fade', x: sx + 5, y: g.t + 10 }, st[1] >= 100 ? 'всем' : st[1] + '%');
|
||
});
|
||
var line = pts.map(function (p) { return x(p[0]).toFixed(1) + ',' + y(p[1]).toFixed(1) });
|
||
g.node('polygon', { 'class': 'zw-fade zw-a-roll', points: line.concat([x(pts[pts.length - 1][0]).toFixed(1) + ',' + g.b, x(pts[0][0]).toFixed(1) + ',' + g.b]).join(' ') });
|
||
g.node('polyline', { 'class': 'zw-draw zw-l-roll', pathLength: 1, points: line.join(' ') });
|
||
for (var i = 0; i <= 4; i++) g.node('text', { 'class': 'zp-xl', x: Math.min(g.r - 22, Math.max(g.l + 18, x(m1 * i / 4))), y: g.h - 4 }, i ? span(Math.round(m1 * i / 4)) : 'выход');
|
||
var cur = g.node('line', { 'class': 'zp-cur', y1: g.t, y2: g.b, visibility: 'hidden' });
|
||
var dot = g.node('circle', { 'class': 'zw-dot', r: 4, visibility: 'hidden' });
|
||
var hit = g.node('rect', { 'class': 'zw-hit', x: g.l, y: 0, width: g.r - g.l, height: g.h });
|
||
function at(ev) {
|
||
var rect = g.svg.getBoundingClientRect(), px = (ev.clientX - rect.left) / rect.width * g.w, best = 0;
|
||
pts.forEach(function (p, k) { if (Math.abs(x(p[0]) - px) < Math.abs(x(pts[best][0]) - px)) best = k });
|
||
var p = pts[best], open = 0;
|
||
(roll.stages || []).forEach(function (st) { if (st[0] <= p[0]) open = st[1] });
|
||
cur.setAttribute('x1', x(p[0])); cur.setAttribute('x2', x(p[0])); cur.setAttribute('visibility', 'visible');
|
||
dot.setAttribute('cx', x(p[0])); dot.setAttribute('cy', y(p[1])); dot.setAttribute('visibility', 'visible');
|
||
var lines = [[color('stable'), ' программ на версии ' + roll.version, pct(p[1] / 1000)]];
|
||
if (open) lines.push([null, open >= 100 ? ' версия открыта всем' : ' программ могли её получить', open >= 100 ? '' : open + '%']);
|
||
g.say(x(p[0]) / g.w * rect.width, p[0] ? 'через ' + span(p[0]) + ' после выхода' : 'в момент выхода', lines);
|
||
}
|
||
hit.addEventListener('pointermove', at);
|
||
hit.addEventListener('pointerdown', at);
|
||
hit.addEventListener('pointerleave', function () { cur.setAttribute('visibility', 'hidden'); dot.setAttribute('visibility', 'hidden'); g.tip.hidden = true });
|
||
|
||
var facts = box.querySelector('.zw-facts'), parts = [];
|
||
if (roll.granted) {
|
||
parts.push('получили ' + fmt(roll.granted));
|
||
parts.push('обновились ' + fmt(roll.confirmed || 0) + ' (' + pct(Math.min(1, (roll.confirmed || 0) / roll.granted)) + ')');
|
||
parts.push('не вышло ' + fmt(roll.failed || 0));
|
||
}
|
||
if (roll.full_sec != null && (roll.stages || []).length > 1) parts.push('открыта всем ' + within(roll.full_sec));
|
||
if (roll.halted) parts.push('раздача остановлена' + (roll.reason ? ': ' + roll.reason : ''));
|
||
facts.hidden = !parts.length;
|
||
facts.textContent = parts.join(' · ');
|
||
facts.classList.toggle('zw-facts-halt', !!roll.halted);
|
||
drawTook(roll, animate);
|
||
}
|
||
|
||
/* сколько занимало обновление: столбцы по корзинам времени */
|
||
function drawTook(roll, animate) {
|
||
var box = q('.zw-took'), hist = roll.took || [], edges = S.took_buckets || [];
|
||
var total = hist.reduce(function (a, b) { return a + b }, 0);
|
||
box.hidden = !total || hist.length !== edges.length + 1;
|
||
if (box.hidden) return;
|
||
var g = stage(box.querySelector('.zw-chart'), 48, animate);
|
||
var top = niceTop(Math.max.apply(null, hist) || 1), slot = (g.r - g.l) / hist.length, bw = Math.min(72, slot * 0.62);
|
||
function y(v) { return g.b - v / top * (g.b - g.t) }
|
||
g.grid(top, short);
|
||
hist.forEach(function (n, i) {
|
||
var cx = g.l + slot * (i + 0.5), col = g.node('g', { 'class': 'zw-bcol zw-grow', style: '--i:' + i * 3 });
|
||
var label = i === 0 ? 'до ' + span(edges[0]) : (i === edges.length ? 'дольше ' + span(edges[i - 1]) : between(edges[i - 1], edges[i]));
|
||
if (n) g.node('rect', { 'class': 'zw-b-done', x: cx - bw / 2, width: bw, y: Math.min(y(n), g.b - 2), height: Math.max(2, g.b - y(n)) }, null, col);
|
||
g.node('text', { 'class': 'zp-xl', x: cx, y: g.h - 4 }, g.w < 560 && i % 2 ? '' : label);
|
||
var hit = g.node('rect', { 'class': 'zw-hit', x: cx - slot / 2, width: slot, y: 0, height: g.h, tabindex: 0 }, null, col);
|
||
function tell() {
|
||
col.classList.add('zw-hot');
|
||
g.say(cx / g.w * g.svg.getBoundingClientRect().width, 'обновление заняло ' + label, [
|
||
[color('done'), ' ' + plural(n, 'программа', 'программы', 'программ'), fmt(n)],
|
||
[null, ' от всех обновившихся', pct(n / total)]
|
||
]);
|
||
}
|
||
function hush() { col.classList.remove('zw-hot'); g.tip.hidden = true }
|
||
hit.addEventListener('pointerenter', tell); hit.addEventListener('focus', tell);
|
||
hit.addEventListener('pointerleave', hush); hit.addEventListener('blur', hush);
|
||
});
|
||
}
|
||
|
||
/* Раздача по ступеням: лестница «5% → 25% → все» с тем, что уже пройдено */
|
||
function drawStage() {
|
||
var box = q('.zw-stage'), rows = box.querySelector('.zw-stage-rows'), now = S.updated || Date.now() / 1000;
|
||
rows.textContent = '';
|
||
CH.forEach(function (c) {
|
||
var r = (S.rollout || {})[c[0]];
|
||
if (!r || (r.ladder || []).length < 2) return;
|
||
/* спокойно розданная версия через сутки с доски уходит; остановленная — висит */
|
||
if (!r.halted && r.percent >= 100 && now - (r.announced || 0) > 86400) return;
|
||
var reached = {};
|
||
(r.stages || []).forEach(function (st) { if (!(st[1] in reached)) reached[st[1]] = st[0] });
|
||
var row = document.createElement('div'); row.className = 'zw-st' + (r.halted ? ' zw-st-halt' : '');
|
||
var name = document.createElement('b'); name.textContent = r.version + ' · ' + c[0]; row.appendChild(name);
|
||
var steps = document.createElement('ol'); steps.className = 'zw-steps';
|
||
r.ladder.forEach(function (p) {
|
||
var li = document.createElement('li'), cur = p === r.percent && (p < 100 || r.halted);
|
||
var state = cur ? (r.halted ? 'halt' : 'now') : (p in reached ? 'done' : 'next');
|
||
li.className = 'zw-step zw-step-' + state;
|
||
var big = document.createElement('span'); big.textContent = p >= 100 ? 'все' : p + '%'; li.appendChild(big);
|
||
var note = document.createElement('small');
|
||
note.textContent = state === 'halt' ? 'остановлена' : state === 'now' ? 'идёт сейчас' : state === 'next' ? 'ждёт' : (reached[p] < 1 ? 'с выхода' : 'через ' + span(reached[p]));
|
||
li.appendChild(note);
|
||
steps.appendChild(li);
|
||
});
|
||
row.appendChild(steps);
|
||
var sum = document.createElement('p');
|
||
sum.textContent = r.halted
|
||
? 'Раздача остановлена' + (r.reason ? ': ' + r.reason : '') + '. Остальным программам эта версия сама не придёт.'
|
||
: 'получили ' + fmt(r.granted || 0) + ' · обновились и вышли на связь ' + fmt(r.confirmed || 0) + ' · не вышло ' + fmt(r.failed || 0);
|
||
row.appendChild(sum);
|
||
rows.appendChild(row);
|
||
});
|
||
box.hidden = !rows.children.length;
|
||
}
|
||
|
||
function drawCharts(animate) { drawOnline(animate); drawUpdates(animate); drawDays(animate); drawRollout(false, animate); drawHistory() }
|
||
|
||
function render(s) {
|
||
if (!s || !s.online) return;
|
||
var online = s.online.total || 0;
|
||
var users = (s.users_today && s.users_today.total) || 0;
|
||
var none = !online && !users && !(s.days || []).length;
|
||
/* пусто: по умолчанию раздела просто нет; если программу выбрали
|
||
сами — честно говорим, что у неё чисел ещё нет */
|
||
if (none && !chosen) return;
|
||
live.classList.toggle('zw-none', none);
|
||
q('.zw-empty').hidden = !none;
|
||
put('scope', PRODUCTS[product].scope);
|
||
if (none) { live.hidden = false; return }
|
||
/* итог прежнего вида: время расхождения было в минутах */
|
||
(s.rollouts || []).forEach(function (r) {
|
||
if (r.half_sec !== undefined || r.half_min === undefined) return;
|
||
r.half_sec = r.half_min == null ? null : r.half_min * 60;
|
||
r.ninety_sec = r.ninety_min == null ? null : r.ninety_min * 60;
|
||
r.curve = (r.curve || []).map(function (p) { return [p[0] * 60, p[1]] });
|
||
});
|
||
S = s;
|
||
live.hidden = false;
|
||
|
||
var el = cell('online');
|
||
if (first && online > 20 && !still) countUp(el, online, 900);
|
||
else el.textContent = fmt(online);
|
||
put('users', fmt(users));
|
||
put('users-word', plural(users, 'пользователь', 'пользователя', 'пользователей'));
|
||
|
||
var latest = s.latest || {}, onLatest = 0;
|
||
(s.online.versions || []).forEach(function (v) { if (v.channel && latest[v.channel] === v.version) onLatest += v.count });
|
||
put('share', online ? pct(onLatest / online) : '—');
|
||
show('latest-dev', !!latest.dev); put('latest-dev', latest.dev || '—');
|
||
show('latest-stable', !!latest.stable); put('latest-stable', latest.stable || '—');
|
||
|
||
var roll = null, timed = null;
|
||
(s.rollouts || []).forEach(function (r) {
|
||
if (!roll && r.half_sec != null) roll = r;
|
||
if (!timed && r.took_median != null) timed = r;
|
||
});
|
||
show('half', !!roll);
|
||
if (roll) {
|
||
put('half-label', roll.version + ' дошла до половины программ');
|
||
put('half', reach(roll, 'half_sec'));
|
||
}
|
||
var queued = ((s.queued && s.queued.dev) || 0) + ((s.queued && s.queued.stable) || 0);
|
||
show('queued', queued > 0); put('queued', fmt(queued));
|
||
/* сервер сам говорит, когда очередь дойдёт: он знает свой канал и вес установщика */
|
||
put('queued-label', 'ждут очереди на скачивание' + (queued > 0 && s.queue_eta ? ', дойдёт примерно за ' + span(Math.max(60, s.queue_eta)) : ''));
|
||
var peak = s.peak_24h;
|
||
show('peak', !!peak && peak.total > online);
|
||
if (peak) { put('peak', fmt(peak.total)); put('peak-label', 'пик за сутки, в ' + clock(peak.t)) }
|
||
var upd = s.updates_24h || { grants: s.grants_24h || 0, done: 0, failed: 0 };
|
||
show('done', upd.done > 0 || upd.grants > 0);
|
||
put('done', fmt(upd.done || upd.grants));
|
||
put('done-label', upd.done ? 'программ обновилось за сутки' + (upd.grants ? ', из ' + fmt(upd.grants) + ' получивших разрешение' : '') : 'обновлений выдано за сутки');
|
||
show('took', !!timed);
|
||
if (timed) put('took', span(timed.took_median));
|
||
show('failed', upd.failed > 0); put('failed', fmt(upd.failed || 0));
|
||
|
||
scene.update(s);
|
||
drawStage();
|
||
drawVersions();
|
||
drawActivity();
|
||
drawTail();
|
||
drawWhy();
|
||
/* графики появляются плавно один раз — когда раздел на экране */
|
||
drawCharts(first && seen);
|
||
first = false;
|
||
}
|
||
|
||
/* сортировка таблиц: заголовок — кнопка; повторный щелчок меняет направление */
|
||
live.querySelectorAll('.zw-tbl th[data-s]').forEach(function (th) {
|
||
var b = document.createElement('button');
|
||
b.type = 'button'; b.textContent = th.textContent;
|
||
th.textContent = ''; th.appendChild(b);
|
||
b.addEventListener('click', function () {
|
||
var table = th.closest('table'), name = table.getAttribute('data-t'), key = th.getAttribute('data-s');
|
||
sorts[name] = [key, sorts[name][0] === key ? -sorts[name][1] : -1];
|
||
if (!S) return;
|
||
if (name === 'days') drawDayTable(); else drawHistory();
|
||
});
|
||
});
|
||
live.querySelectorAll('.zw-zoom button').forEach(function (b) {
|
||
b.addEventListener('click', function () { zoom = +b.getAttribute('data-z'); if (S) drawRollout(true, true) });
|
||
});
|
||
if (window.IntersectionObserver) {
|
||
var io = new IntersectionObserver(function (es) {
|
||
if (seen || !es.some(function (e) { return e.isIntersecting })) return;
|
||
seen = true; io.disconnect();
|
||
if (S) drawCharts(true);
|
||
}, { threshold: 0.12 });
|
||
io.observe(live);
|
||
} else seen = true;
|
||
|
||
live.querySelectorAll('.zw-ctl .zp-range button').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
range = b.getAttribute('data-r');
|
||
live.querySelectorAll('.zw-ctl .zp-range button').forEach(function (x) { x.setAttribute('aria-pressed', x === b ? 'true' : 'false') });
|
||
if (S) { drawOnline(true); drawUpdates(true) }
|
||
});
|
||
});
|
||
live.querySelectorAll('.zw-legend .zp-lg').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
var key = b.getAttribute('data-c');
|
||
/* последний включённый канал выключить нельзя: пустой график ничего не говорит */
|
||
if (on[key] && !CH.some(function (c) { return c[0] !== key && on[c[0]] })) return;
|
||
on[key] = !on[key];
|
||
b.setAttribute('aria-pressed', on[key] ? 'true' : 'false');
|
||
if (S) { drawOnline(true); drawDays(true) }
|
||
});
|
||
});
|
||
var resizing = 0;
|
||
window.addEventListener('resize', function () {
|
||
clearTimeout(resizing);
|
||
resizing = setTimeout(function () { if (S) drawCharts() }, 150);
|
||
});
|
||
|
||
function load() {
|
||
if (document.hidden) return;
|
||
var asked = product;
|
||
fetch(PRODUCTS[asked].url + '?m=' + Math.floor(Date.now() / 60000))
|
||
.then(function (r) { if (!r.ok) throw 0; return r.json() })
|
||
/* ответ про программу, с которой уже ушли, не показываем */
|
||
.then(function (s) { if (asked === product) render(s) })
|
||
/* итога нет вовсе: у выбранной программы это то же «чисел ещё нет» */
|
||
.catch(function () { if (asked === product && chosen && !S) render({ online: { total: 0 } }) });
|
||
}
|
||
live.querySelectorAll('.zw-prod button').forEach(function (b) {
|
||
/* у программы без своего итога кнопки нет вовсе */
|
||
if (!PRODUCTS[b.getAttribute('data-p')].url) { b.hidden = true; return }
|
||
b.addEventListener('click', function () {
|
||
var next = b.getAttribute('data-p');
|
||
if (next === product) return;
|
||
product = next; chosen = true;
|
||
live.querySelectorAll('.zw-prod button').forEach(function (x) { x.setAttribute('aria-pressed', x === b ? 'true' : 'false') });
|
||
/* графики новой программы появляются заново, выбор выпуска и отрезка сбрасывается */
|
||
S = null; first = true; picked = ''; zoom = -1;
|
||
scene.reset();
|
||
load();
|
||
});
|
||
});
|
||
load();
|
||
setInterval(load, 60000);
|
||
document.addEventListener('visibilitychange', function () { if (!document.hidden) load() });
|
||
})();
|
||
|
||
/* ── Пауза анимаций вне экрана ──────────────────────────────────────── */
|
||
(function () {
|
||
if (!window.IntersectionObserver) return;
|
||
var blocks = document.querySelectorAll('.zpm-home > section, .zpm-home > div:not(.zx), #zpm-duel, #zg-packets');
|
||
var io = new IntersectionObserver(function (es) {
|
||
es.forEach(function (e) { e.target.classList.toggle('zoff', !e.isIntersecting) });
|
||
}, { rootMargin: '150px 0px' });
|
||
blocks.forEach(function (b) { io.observe(b) });
|
||
})();
|
||
})();
|