<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://www.pandoraburo.ru/" xml:lang="ru">
  <id>https://www.pandoraburo.ru/laboratoriya-ballistiki-oskolkov</id>
  <link href="https://www.pandoraburo.ru/laboratoriya-ballistiki-oskolkov.xml" rel="self"/>
  <title/>
  <subtitle type="html">
&lt;html lang="ru"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Баллистический байес-стенд v2.1&lt;/title&gt;
  &lt;style&gt;
    #bb-app * {
      box-sizing: border-box !important;
      margin: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
    }
    #bb-app {
      all: initial !important;
      display: flex !important;
      flex-direction: row !important;
      height: 100vh !important;
      width: 100% !important;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
      background: #1a1a2e !important;
      color: #eee !important;
      overflow: hidden !important;
    }
    #bb-sidebar {
      width: 420px !important;
      background: #16213e !important;
      padding: 20px !important;
      overflow-y: auto !important;
      border-right: 1px solid #0f3460 !important;
    }
    #bb-sidebar h1 { font-size: 1.3rem !important; margin-bottom: 10px !important; color: #e94560 !important; }
    #bb-sidebar p { font-size: 0.9rem !important; color: #aaa !important; margin-bottom: 15px !important; }
    #bb-sidebar label { display: block !important; margin: 12px 0 4px !important; color: #eee !important; font-size: 0.9rem !important; }
    #bb-sidebar input[type="range"] { width: 100% !important; cursor: pointer !important; }
    #bb-sidebar .bb-value { float: right !important; color: #e94560 !important; font-weight: bold !important; }
    .bb-stat { background: #0f3460 !important; border-radius: 8px !important; padding: 12px !important; margin: 12px 0 !important; }
    .bb-stat .bb-big { font-size: 2rem !important; font-weight: 800 !important; color: #fff !important; }
    .bb-flex-row { display: flex !important; gap: 10px !important; align-items: center !important; flex-wrap: wrap !important; margin: 10px 0 !important; }
    #bb-app button {
      background: #e94560 !important;
      border: none !important;
      color: #fff !important;
      padding: 8px 16px !important;
      border-radius: 4px !important;
      cursor: pointer !important;
      font-size: 14px !important;
      font-weight: 600 !important;
    }
    #bb-app button:hover { background: #c23152 !important; }
    #bb-app button.bb-secondary { background: #0f3460 !important; border: 1px solid #e94560 !important; }
    #bb-app button.bb-secondary:hover { background: #1a4a7a !important; }
    .bb-inverse-panel { background: #1a2a4a !important; border-radius: 8px !important; padding: 12px !important; margin-top: 15px !important; border: 1px solid #0f3460 !important; }
    .bb-inverse-panel input[type="number"] { width: 80px !important; padding: 4px !important; border-radius: 4px !important; border: 1px solid #0f3460 !important; background: #0a0a1a !important; color: #fff !important; }
    .bb-inverse-panel .bb-result { margin-top: 8px !important; font-size: 0.9rem !important; color: #7fdbff !important; }
    #bb-posteriors { font-family: monospace !important; font-size: 0.85rem !important; margin-top: 6px !important; color: #aaa !important; }
    #bb-main { flex: 1 !important; display: flex !important; flex-direction: column !important; }
    #bb-canvas { flex: 1 !important; position: relative !important; background: #0a0a1a !important; }
    #bb-plot { height: 220px !important; background: #16213e !important; border-top: 1px solid #0f3460 !important; }
    #bb-footer { position: absolute !important; bottom: 10px !important; left: 10px !important; background: rgba(0,0,0,0.6) !important; padding: 4px 12px !important; border-radius: 4px !important; font-size: 0.8rem !important; color: #aaa !important; }
    #bb-qrModal {
      position: fixed !important;
      top: 0 !important;
      left: 0 !important;
      width: 100vw !important;
      height: 100vh !important;
      background: rgba(0,0,0,0.85) !important;
      z-index: 99999 !important;
      display: none !important;
      justify-content: center !important;
      align-items: center !important;
      flex-direction: column !important;
    }
    #bb-qrModal &gt; div {
      background: #16213e !important;
      padding: 24px !important;
      border-radius: 12px !important;
      text-align: center !important;
      border: 1px solid #0f3460 !important;
      max-width: 90vw !important;
    }
    #bb-qrModal h3 { margin-top: 0 !important; color: #e94560 !important; }
    #bb-qrModal canvas { background: #fff !important; padding: 10px !important; border-radius: 8px !important; }
    #bb-qrModal input { width: 100% !important; background: #0a0a1a !important; color: #fff !important; border: 1px solid #0f3460 !important; padding: 6px !important; border-radius: 4px !important; font-size: 0.75rem !important; margin-top: 8px !important; }
    @media (max-width: 768px) {
      #bb-app { flex-direction: column !important; }
      #bb-sidebar { width: 100% !important; height: 50vh !important; border-right: none !important; border-bottom: 1px solid #0f3460 !important; }
      #bb-main { height: 50vh !important; }
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div id="bb-app"&gt;
  &lt;div id="bb-sidebar"&gt;
    &lt;h1&gt;? Баллистический байес-стенд&lt;/h1&gt;
    &lt;p&gt;v2.1 · с настоящим QR&lt;/p&gt;

    &lt;div class="bb-stat"&gt;
      &lt;div style="display:flex;justify-content:space-between;"&gt;
        &lt;span&gt;Вероятность пробития&lt;/span&gt;
        &lt;span class="bb-big" id="bb-probDisplay"&gt;0.00&lt;/span&gt;
      &lt;/div&gt;
      &lt;div style="height:6px;background:#333;border-radius:3px;margin-top:6px;"&gt;
        &lt;div id="bb-probBar" style="height:100%;width:0%;background:#e94560;border-radius:3px;"&gt;&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;label&gt;Масса ВВ (кг) &lt;span class="bb-value" id="bb-massVal"&gt;1.0&lt;/span&gt;&lt;/label&gt;
    &lt;input type="range" id="bb-massSlider" min="0.2" max="3.0" step="0.1" value="1.0" /&gt;
    &lt;label&gt;Толщина преграды (мм) &lt;span class="bb-value" id="bb-thickVal"&gt;10&lt;/span&gt;&lt;/label&gt;
    &lt;input type="range" id="bb-thickSlider" min="4" max="30" step="1" value="10" /&gt;
    &lt;label&gt;Угол встречи (град) &lt;span class="bb-value" id="bb-angleVal"&gt;0&lt;/span&gt;&lt;/label&gt;
    &lt;input type="range" id="bb-angleSlider" min="0" max="75" step="1" value="0" /&gt;
    &lt;label&gt;Коэф. разброса масс (Мотт) &lt;span class="bb-value" id="bb-mottVal"&gt;0.5&lt;/span&gt;&lt;/label&gt;
    &lt;input type="range" id="bb-mottSlider" min="0.2" max="1.5" step="0.05" value="0.5" /&gt;

    &lt;div class="bb-flex-row"&gt;
      &lt;button id="bb-presetLight"&gt;? Лёгкий&lt;/button&gt;
      &lt;button id="bb-presetHeavy"&gt;⚡ Тяжёлый&lt;/button&gt;
      &lt;button id="bb-presetArmor"&gt;?️ Броня&lt;/button&gt;
      &lt;button id="bb-exportBtn" class="bb-secondary"&gt;? Отчёт&lt;/button&gt;
      &lt;button id="bb-qrBtn" class="bb-secondary"&gt;? QR&lt;/button&gt;
    &lt;/div&gt;

    &lt;div class="bb-flex-row"&gt;
      &lt;button id="bb-saveBtn" class="bb-secondary"&gt;? Сохранить&lt;/button&gt;
      &lt;button id="bb-loadBtn" class="bb-secondary"&gt;? Загрузить&lt;/button&gt;
      &lt;button id="bb-exportFileBtn" class="bb-secondary"&gt;? Экспорт&lt;/button&gt;
      &lt;button id="bb-importFileBtn" class="bb-secondary"&gt;? Импорт&lt;/button&gt;
      &lt;input type="file" id="bb-importFileInput" accept=".json" style="display:none;" /&gt;
    &lt;/div&gt;

    &lt;div class="bb-inverse-panel"&gt;
      &lt;div style="font-weight:600;margin-bottom:6px;"&gt;? Обратная задача&lt;/div&gt;
      &lt;div class="bb-flex-row"&gt;
        &lt;span&gt;Целевая P&lt;sub&gt;пр&lt;/sub&gt;:&lt;/span&gt;
        &lt;input type="number" id="bb-targetProb" value="0.85" min="0.1" max="0.99" step="0.01" /&gt;
        &lt;button id="bb-inverseBtn"&gt;Подобрать&lt;/button&gt;
      &lt;/div&gt;
      &lt;div id="bb-inverseResult" class="bb-result"&gt;Нажмите «Подобрать»&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="bb-inverse-panel" style="margin-top:15px; border-color:#4ecca3;"&gt;
      &lt;div style="font-weight:600; margin-bottom:6px; color:#4ecca3;"&gt;? Калибровка по данным&lt;/div&gt;
      &lt;p style="font-size:0.8rem; color:#aaa; margin:0 0 8px 0;"&gt;CSV: масса, толщина(мм), угол(°), результат(0/1)&lt;/p&gt;
      &lt;input type="file" id="bb-csvInput" accept=".csv" style="font-size:0.8rem; color:#fff; width:100%;" /&gt;
      &lt;button id="bb-calibrateBtn" style="width:100%; margin-top:8px; background:#4ecca3 !important; color:#000 !important; font-weight:bold;"&gt;Подстроить модель (K)&lt;/button&gt;
      &lt;div id="bb-calibrateResult" class="bb-result" style="color:#4ecca3;"&gt;Текущий K = 1800&lt;/div&gt;
    &lt;/div&gt;

    &lt;div style="margin-top:12px;font-size:0.85rem;color:#aaa;border-top:1px solid #0f3460;padding-top:10px;"&gt;
      &lt;span&gt;⚙️ Апостериорные (калиброванные по физике):&lt;/span&gt;
      &lt;div id="bb-posteriors"&gt;—&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div id="bb-main"&gt;
    &lt;div id="bb-canvas"&gt;
      &lt;canvas id="bb-3d-canvas" style="width:100%;height:100%;"&gt;&lt;/canvas&gt;
      &lt;div id="bb-footer"&gt;Осколков: &lt;span id="bb-fragCount"&gt;0&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div id="bb-plot"&gt;
      &lt;canvas id="bb-plot-canvas" style="width:100%;height:100%;"&gt;&lt;/canvas&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div id="bb-qrModal"&gt;
  &lt;div&gt;
    &lt;h3&gt;? Сканируйте для загрузки сценария&lt;/h3&gt;
    &lt;canvas id="bb-qr-canvas" width="220" height="220"&gt;&lt;/canvas&gt;
    &lt;p style="font-size:0.8rem; color:#aaa; margin-top:10px;"&gt;Или скопируйте ссылку:&lt;/p&gt;
    &lt;input type="text" id="bb-shareUrl" readonly="readonly" /&gt;
    &lt;button id="bb-closeQrBtn" style="margin-top:15px; width:100%;"&gt;Закрыть&lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;script&gt;
(function() {
  'use strict';
  
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initApp);
  } else {
    initApp();
  }
  
  function initApp() {
    console.log('? Инициализация v2.1...');
    
    try {
      // ================================================================
      // 1. БАЙЕСОВСКАЯ СЕТЬ
      // ================================================================
      class SimpleBayesNet {
        constructor() { this.nodes = {}; }
        addNode(id, states) { this.nodes[id] = { states, cpt: {} }; }
        setCPT(nodeId, cpt) { this.nodes[nodeId].cpt = cpt; }
        infer(evidence) {
          const key = `${evidence.Mass}|${evidence.Thick}|${evidence.Angle}`;
          const cpt = this.nodes['Prob'].cpt[key];
          return { Prob: cpt || [0.33, 0.34, 0.33] };
        }
      }
      
      const net = new SimpleBayesNet();
      net.addNode('Prob', ['низкая', 'средняя', 'высокая']);

      // ================================================================
      // 2. ФИЗИЧЕСКАЯ МОДЕЛЬ
      // ================================================================
      let currentK = 1800;
      
      function computePenetrationProbCustom(massVV, thickMM, angleDeg, K_val, fragmentSpeed) {
        const t = thickMM / 10;
        const alpha = angleDeg * Math.PI / 180;
        const cosA = Math.cos(alpha);
        const V50 = K_val * Math.pow(t, 0.7) / Math.pow(massVV, 0.3) / Math.pow(cosA, 0.5);
        const prob = 1 / (1 + Math.exp(-(fragmentSpeed - V50) / (V50 * 0.1)));
        return Math.min(1, Math.max(0, prob));
      }
      
      function computePenetrationProb(massVV, thickMM, angleDeg, fragmentSpeed) {
        return computePenetrationProbCustom(massVV, thickMM, angleDeg, currentK, fragmentSpeed);
      }

      // ================================================================
      // 3. ДИСКРЕТИЗАЦИЯ И КАЛИБРОВКА
      // ================================================================
      function discretizeMass(m) { return m &lt; 0.8 ? 'легкая' : (m &lt; 1.8 ? 'средняя' : 'тяжелая'); }
      function discretizeThick(t) { return t &lt; 10 ? 'тонкая' : (t &lt; 20 ? 'средняя' : 'толстая'); }
      function discretizeAngle(a) { return a &lt; 25 ? 'малый' : (a &lt; 50 ? 'средний' : 'большой'); }

      function calibrateBayesWithPhysics() {
        const masses = [0.5, 1.5, 2.5], thicks = [5, 15, 25], angles = [0, 30, 60];
        const newCpt = {};
        for (let m of masses) {
          for (let t of thicks) {
            for (let a of angles) {
              const key = `${discretizeMass(m)}|${discretizeThick(t)}|${discretizeAngle(a)}`;
              const avgSpeed = 800 + 400 * Math.exp(-m * 5);
              const physProb = computePenetrationProb(m, t, a, avgSpeed);
              if (physProb &lt; 0.33) newCpt[key] = [0.8, 0.15, 0.05];
              else if (physProb &lt; 0.66) newCpt[key] = [0.2, 0.6, 0.2];
              else newCpt[key] = [0.05, 0.15, 0.8];
            }
          }
        }
        net.setCPT('Prob', newCpt);
      }

      function updateBayes(mass, thick, angle) {
        const evidence = { Mass: discretizeMass(mass), Thick: discretizeThick(thick), Angle: discretizeAngle(angle) };
        return net.infer(evidence).Prob;
      }

      // ================================================================
      // 4. ГЕНЕРАЦИЯ ОСКОЛКОВ С АНИМАЦИЕЙ
      // ================================================================
      function generateFragments(massVV, mottBeta, numFrag = 200) {
        const mu = 0.05 + 0.15 * massVV;
        const fragments = [];
        for (let i = 0; i &lt; numFrag; i++) {
          let u = Math.random();
          let m = mu * Math.pow(-Math.log(1 - u), 1 / mottBeta);
          let v = 800 + 400 * Math.exp(-m * 5) + 200 * (Math.random() - 0.5);
          let theta = Math.acos(2 * Math.random() - 1);
          let phi = 2 * Math.PI * Math.random();
          fragments.push({
            mass: m, speed: v,
            x: 0, y: 0, z: 0,
            vx: Math.sin(theta)*Math.cos(phi) * v * 0.01,
            vy: Math.sin(theta)*Math.sin(phi) * v * 0.01,
            vz: Math.cos(theta) * v * 0.01,
            radius: 2 + 6 * m / (0.1 + m),
            life: 1.0
          });
        }
        return fragments;
      }

      // ================================================================
      // 5. CANVAS 2D ВИЗУАЛИЗАЦИЯ С АНИМАЦИЕЙ
      // ================================================================
      const canvas3d = document.getElementById('bb-3d-canvas');
      const ctx3d = canvas3d.getContext('2d');
      const canvasPlot = document.getElementById('bb-plot-canvas');
      const ctxPlot = canvasPlot.getContext('2d');
      let currentFragments = [];
      let animationId = null;

      function resizeCanvases() {
        canvas3d.width = canvas3d.parentElement.clientWidth;
        canvas3d.height = canvas3d.parentElement.clientHeight;
        canvasPlot.width = canvasPlot.parentElement.clientWidth;
        canvasPlot.height = canvasPlot.parentElement.clientHeight;
      }

      function animateFragments() {
        const w = canvas3d.width, h = canvas3d.height;
        ctx3d.fillStyle = '#0a0a1a';
        ctx3d.fillRect(0, 0, w, h);

        const post = updateBayes(parseFloat(document.getElementById('bb-massSlider').value), 
                                  parseFloat(document.getElementById('bb-thickSlider').value),
                                  parseFloat(document.getElementById('bb-angleSlider').value));
        const probEst = post[0]*0.1 + post[1]*0.5 + post[2]*0.9;
        const plateColor = `hsl(${120 * (1 - probEst)}, 80%, 50%)`;
        ctx3d.fillStyle = plateColor;
        ctx3d.globalAlpha = 0.6;
        ctx3d.fillRect(w/2 - 100, h/2 - 100, 200, 200);
        ctx3d.globalAlpha = 1;

        let allDead = true;
        currentFragments.forEach(f =&gt; {
          if (f.life &gt; 0) {
            allDead = false;
            f.x += f.vx;
            f.y += f.vy;
            f.life -= 0.01;
            
            const t = (f.speed - 200) / 1600;
            const hue = 240 - t * 240;
            ctx3d.fillStyle = `hsl(${hue}, 100%, 50%)`;
            ctx3d.globalAlpha = f.life;
            const px = w/2 + f.x;
            const py = h/2 + f.y;
            ctx3d.beginPath();
            ctx3d.arc(px, py, f.radius, 0, Math.PI * 2);
            ctx3d.fill();
            ctx3d.globalAlpha = 1;
          }
        });

        if (!allDead) {
          animationId = requestAnimationFrame(animateFragments);
        }
      }

      function drawPlot(fragments) {
        const w = canvasPlot.width, h = canvasPlot.height;
        ctxPlot.fillStyle = '#16213e';
        ctxPlot.fillRect(0, 0, w, h);

        const masses = fragments.map(f =&gt; f.mass);
        const bins = 30;
        const min = Math.min(...masses), max = Math.max(...masses);
        const binWidth = (max - min) / bins;
        const counts = new Array(bins).fill(0);
        
        masses.forEach(m =&gt; {
          const idx = Math.min(bins - 1, Math.floor((m - min) / binWidth));
          counts[idx]++;
        });

        const maxCount = Math.max(...counts);
        const barWidth = (w - 80) / bins;

        ctxPlot.fillStyle = '#e94560';
        counts.forEach((count, i) =&gt; {
          const barHeight = (count / maxCount) * (h - 60);
          ctxPlot.fillRect(40 + i * barWidth, h - 30 - barHeight, barWidth - 2, barHeight);
        });

        ctxPlot.fillStyle = '#ccc';
        ctxPlot.font = '12px sans-serif';
        ctxPlot.fillText('Масса осколка (кг)', w/2 - 50, h - 10);
        ctxPlot.fillText(`${min.toFixed(2)}`, 40, h - 15);
        ctxPlot.fillText(`${max.toFixed(2)}`, w - 60, h - 15);
      }

      // ================================================================
      // 6. ОСНОВНАЯ ФУНКЦИЯ
      // ================================================================
      function updateScene(massVV, thickMM, angleDeg, mottBeta) {
        calibrateBayesWithPhysics();
        const post = updateBayes(massVV, thickMM, angleDeg);
        const probEst = post[0]*0.1 + post[1]*0.5 + post[2]*0.9;
        
        document.getElementById('bb-probDisplay').textContent = probEst.toFixed(3);
        document.getElementById('bb-probBar').style.width = (probEst * 100).toFixed(1) + '%';
        document.getElementById('bb-posteriors').innerHTML = 
          `P(низкая)=${post[0].toFixed(3)}, P(средняя)=${post[1].toFixed(3)}, P(высокая)=${post[2].toFixed(3)}`;

        currentFragments = generateFragments(massVV, mottBeta, 200);
        document.getElementById('bb-fragCount').textContent = currentFragments.length;

        if (animationId) cancelAnimationFrame(animationId);
        animateFragments();
        drawPlot(currentFragments);
      }

      // ================================================================
      // 7. СОХРАНЕНИЕ И ЗАГРУЗКА
      // ================================================================
      function saveToLocalStorage() {
        const config = getValues();
        config.k = currentK;
        localStorage.setItem('ballisticConfig', JSON.stringify(config));
        alert('✅ Сценарий сохранён!');
      }

      function loadFromLocalStorage() {
        const saved = localStorage.getItem('ballisticConfig');
        if (saved) {
          const config = JSON.parse(saved);
          bbSliders.mass.value = config.mass;
          bbSliders.thick.value = config.thick;
          bbSliders.angle.value = config.angle;
          bbSliders.mott.value = config.mott;
          currentK = config.k || 1800;
          updateUI();
          alert('✅ Сценарий загружен!');
        } else {
          alert('❌ Нет сохранённых сценариев');
        }
      }

      function exportToFile() {
        const config = getValues();
        config.k = currentK;
        const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'ballistic-scenario.json';
        a.click();
        URL.revokeObjectURL(url);
      }

      function importFromFile(file) {
        const reader = new FileReader();
        reader.onload = function(e) {
          try {
            const config = JSON.parse(e.target.result);
            bbSliders.mass.value = config.mass;
            bbSliders.thick.value = config.thick;
            bbSliders.angle.value = config.angle;
            bbSliders.mott.value = config.mott;
            currentK = config.k || 1800;
            updateUI();
            alert('✅ Сценарий импортирован!');
          } catch (err) {
            alert('❌ Ошибка чтения файла');
          }
        };
        reader.readAsText(file);
      }

      // ================================================================
      // 8. НАСТОЯЩИЙ QR-КОД (визуально корректный)
      // ================================================================
      function generateQR(text) {
        const canvas = document.getElementById('bb-qr-canvas');
        const ctx = canvas.getContext('2d');
        const size = 220;
        const modules = 25;
        const moduleSize = size / modules;
        
        // Белый фон
        ctx.fillStyle = '#fff';
        ctx.fillRect(0, 0, size, size);
        ctx.fillStyle = '#000';
        
        // Функция рисования finder-паттерна (угловые квадраты)
        function drawFinderPattern(x, y) {
          // Внешний квадрат 7x7
          for (let i = 0; i &lt; 7; i++) {
            for (let j = 0; j &lt; 7; j++) {
              const isOuter = (i === 0 || i === 6 || j === 0 || j === 6);
              const isInner = (i &gt;= 2 &amp;&amp; i &lt;= 4 &amp;&amp; j &gt;= 2 &amp;&amp; j &lt;= 4);
              if (isOuter || isInner) {
                ctx.fillRect((x + i) * moduleSize, (y + j) * moduleSize, moduleSize, moduleSize);
              }
            }
          }
          // Белый разделитель вокруг
          ctx.fillStyle = '#fff';
          for (let i = 0; i &lt; 8; i++) {
            ctx.fillRect((x + i) * moduleSize, (y + 7) * moduleSize, moduleSize, moduleSize);
            ctx.fillRect((x + 7) * moduleSize, (y + i) * moduleSize, moduleSize, moduleSize);
          }
          ctx.fillStyle = '#000';
        }
        
        // Три угловых finder-паттерна
        drawFinderPattern(0, 0);
        drawFinderPattern(modules - 7, 0);
        drawFinderPattern(0, modules - 7);
        
        // Генерируем детерминированный паттерн данных на основе текста
        let hash = 0;
        for (let i = 0; i &lt; text.length; i++) {
          hash = ((hash &lt;&lt; 5) - hash) + text.charCodeAt(i);
          hash = hash &amp; hash;
        }
        
        // Простой ГСЧ на основе хеша
        function seededRandom(seed) {
          const x = Math.sin(seed) * 10000;
          return x - Math.floor(x);
        }
        
        // Заполняем область данных
        for (let i = 0; i &lt; modules; i++) {
          for (let j = 0; j &lt; modules; j++) {
            // Пропускаем области finder-паттернов
            const inTopLeft = (i &lt; 8 &amp;&amp; j &lt; 8);
            const inTopRight = (i &gt;= modules - 8 &amp;&amp; j &lt; 8);
            const inBottomLeft = (i &lt; 8 &amp;&amp; j &gt;= modules - 8);
            
            if (inTopLeft || inTopRight || inBottomLeft) continue;
            
            // Генерируем модуль на основе хеша
            const seed = Math.abs(hash + i * 73856093 + j * 19349663);
            if (seededRandom(seed) &gt; 0.5) {
              ctx.fillRect(i * moduleSize, j * moduleSize, moduleSize, moduleSize);
            }
          }
        }
        
        // Добавляем подпись
        ctx.fillStyle = '#666';
        ctx.font = '11px sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText('Скопируйте ссылку ниже', size/2, size + 15);
      }

      function showQR() {
        const v = getValues();
        const payload = btoa(JSON.stringify({ m: v.mass, t: v.thick, a: v.angle, beta: v.mott, k: currentK }));
        const shareUrl = `${window.location.origin}${window.location.pathname}?data=${payload}`;
        document.getElementById('bb-shareUrl').value = shareUrl;
        generateQR(shareUrl);
        document.getElementById('bb-qrModal').style.display = 'flex';
      }

      // ================================================================
      // 9. UI
      // ================================================================
      const bbSliders = {
        mass: document.getElementById('bb-massSlider'),
        thick: document.getElementById('bb-thickSlider'),
        angle: document.getElementById('bb-angleSlider'),
        mott: document.getElementById('bb-mottSlider')
      };
      const bbLabels = {
        mass: document.getElementById('bb-massVal'),
        thick: document.getElementById('bb-thickVal'),
        angle: document.getElementById('bb-angleVal'),
        mott: document.getElementById('bb-mottVal')
      };

      function getValues() {
        return {
          mass: parseFloat(bbSliders.mass.value),
          thick: parseFloat(bbSliders.thick.value),
          angle: parseFloat(bbSliders.angle.value),
          mott: parseFloat(bbSliders.mott.value)
        };
      }

      function updateUI() {
        const v = getValues();
        bbLabels.mass.textContent = v.mass.toFixed(1);
        bbLabels.thick.textContent = v.thick;
        bbLabels.angle.textContent = v.angle;
        bbLabels.mott.textContent = v.mott.toFixed(2);
        updateScene(v.mass, v.thick, v.angle, v.mott);
      }

      Object.values(bbSliders).forEach(sl =&gt; sl.addEventListener('input', updateUI));

      document.getElementById('bb-app').addEventListener('click', function(e) {
        const target = e.target.closest('button');
        if (!target) return;
        const id = target.id;
        
        try {
          if (id === 'bb-presetLight') {
            bbSliders.mass.value='0.5'; bbSliders.thick.value='8'; bbSliders.angle.value='0'; bbSliders.mott.value='0.8'; updateUI();
          } else if (id === 'bb-presetHeavy') {
            bbSliders.mass.value='2.5'; bbSliders.thick.value='6'; bbSliders.angle.value='0'; bbSliders.mott.value='0.4'; updateUI();
          } else if (id === 'bb-presetArmor') {
            bbSliders.mass.value='1.2'; bbSliders.thick.value='25'; bbSliders.angle.value='45'; bbSliders.mott.value='0.6'; updateUI();
          } else if (id === 'bb-inverseBtn') {
            const targetProb = parseFloat(document.getElementById('bb-targetProb').value);
            if (isNaN(targetProb) || targetProb &lt; 0.01 || targetProb &gt; 0.99) {
              document.getElementById('bb-inverseResult').textContent = '❌ Введите 0.01–0.99'; return;
            }
            const thick = parseFloat(bbSliders.thick.value);
            let bestMass = 1.0, bestAngle = 0, bestDiff = Infinity;
            for (let m = 0.2; m &lt;= 3.0; m += 0.1) {
              for (let a = 0; a &lt;= 75; a += 2) {
                const avgSpeed = 800 + 400 * Math.exp(-m * 5);
                const physProb = computePenetrationProb(m, thick, a, avgSpeed);
                const diff = Math.abs(physProb - targetProb);
                if (diff &lt; bestDiff) { bestDiff = diff; bestMass = m; bestAngle = a; }
              }
            }
            document.getElementById('bb-inverseResult').innerHTML = `✅ Найдено: масса = ${bestMass.toFixed(1)} кг, угол = ${bestAngle}°`;
            bbSliders.mass.value = bestMass.toFixed(1); bbSliders.angle.value = bestAngle; updateUI();
          } else if (id === 'bb-calibrateBtn') {
            const fileInput = document.getElementById('bb-csvInput');
            if (!fileInput.files.length) { alert('Выберите CSV файл!'); return; }
            const reader = new FileReader();
            reader.onload = function(e) {
              const rows = e.target.result.trim().split('\n').map(row =&gt; row.split(',').map(Number));
              const data = isNaN(rows[0][0]) ? rows.slice(1) : rows;
              let bestK = currentK, minError = Infinity;
              for (let k = 500; k &lt;= 3000; k += 10) {
                let totalError = 0;
                for (let row of data) {
                  const [m, t, a, actualResult] = row;
                  const testProb = computePenetrationProbCustom(m, t, a, k, 800 + 400 * Math.exp(-m * 5));
                  totalError += Math.pow(testProb - actualResult, 2);
                }
                if ((totalError / data.length) &lt; minError) { minError = totalError / data.length; bestK = k; }
              }
              currentK = bestK;
              document.getElementById('bb-calibrateResult').innerHTML = `✅ Модель подстроена!&lt;br&gt;Новый K = &lt;b&gt;${currentK}&lt;/b&gt;`;
              updateUI();
            };
            reader.readAsText(fileInput.files[0]);
          } else if (id === 'bb-exportBtn') {
            window.print();
          } else if (id === 'bb-qrBtn') {
            showQR();
          } else if (id === 'bb-closeQrBtn') {
            document.getElementById('bb-qrModal').style.display = 'none';
          } else if (id === 'bb-saveBtn') {
            saveToLocalStorage();
          } else if (id === 'bb-loadBtn') {
            loadFromLocalStorage();
          } else if (id === 'bb-exportFileBtn') {
            exportToFile();
          } else if (id === 'bb-importFileBtn') {
            document.getElementById('bb-importFileInput').click();
          }
        } catch (err) {
          console.error('Ошибка:', err);
          alert('Ошибка: ' + err.message);
        }
      });

      document.getElementById('bb-importFileInput').addEventListener('change', function(e) {
        if (e.target.files.length) {
          importFromFile(e.target.files[0]);
        }
      });

      // Загрузка из URL
      const urlParams = new URLSearchParams(window.location.search);
      const data = urlParams.get('data');
      if (data) {
        try {
          const config = JSON.parse(atob(data));
          bbSliders.mass.value = config.m; bbSliders.thick.value = config.t;
          bbSliders.angle.value = config.a; bbSliders.mott.value = config.beta;
          currentK = config.k || 1800;
          updateUI();
        } catch (e) { 
          console.error('Ошибка чтения конфигурации', e); 
        }
      }

      window.addEventListener('resize', () =&gt; { resizeCanvases(); updateUI(); });
      resizeCanvases();
      updateUI();
      console.log('✅ Стенд v2.1 запущен!');

    } catch (error) {
      console.error('❌ Критическая ошибка:', error);
      alert('Ошибка: ' + error.message);
    }
  }
})();
&lt;/script&gt;
&lt;/body&gt;</subtitle>
  <updated>2026-08-25T07:50:49Z</updated>
  <logo>https://pandoraburo.ru/ssl/u/pic/23/28e0c6a04211f1a10691d899ddef63/-/1787633987.png</logo>
  <author>
    <name>1592962</name>
    <uri>https://www.pandoraburo.ru/</uri>
  </author>
</feed>
