Загрузка данных


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Pink AR v21</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { height:100%; background:#000; color:#fff; overflow:hidden;
    font-family:-apple-system,sans-serif;
    -webkit-user-select:none; user-select:none;
    -webkit-tap-highlight-color:transparent; touch-action:manipulation; }

  #tapper { position:fixed; top:0; left:0; right:0; bottom:0; background:#000;
    display:flex; align-items:center; justify-content:center; z-index:500; }
  #visual { padding:36px 52px;
    background:linear-gradient(135deg,#ff2d95,#b81aff);
    color:#fff; font-size:22px; font-weight:700;
    border-radius:40px; text-align:center;
    box-shadow:0 10px 40px rgba(255,45,149,.5); pointer-events:none; }

  /* HUD: слева текст, справа FPS */
  #hud {
    position:fixed; top:0; left:0; right:0;
    background:rgba(0,0,0,.55);
    font-family:ui-monospace,Menlo,monospace; font-size:12px; line-height:1.4;
    z-index:600;
    pointer-events:none;
    transition:background .15s;
    display:flex; align-items:center;
    padding:8px 12px;
  }
  #hud.hit { background:rgba(255,45,149,.9); }
  #hudText {
    flex:1;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    color:#fff;
  }
  #fps {
    margin-left:12px;
    color:#9effa0;
    font-weight:600;
    min-width:70px; text-align:right;
    flex:0 0 auto;
  }
  #fps.mid { color:#ffe066; }
  #fps.low { color:#ff7a7a; }

  /* AR-бокс — контейнер для видео */
  #arBox {
    position:fixed; top:0; left:0;
    width:100px; height:100px;
    transform-origin:0 0;
    overflow:hidden;
    z-index:2;
    visibility:hidden;
    pointer-events:none;
    background:#000;
    /* GPU hint — держим элемент в отдельном слое */
    transform: translateZ(0);
    -webkit-transform: translateZ(0);
    backface-visibility:hidden;
    -webkit-backface-visibility:hidden;
    contain: strict;
  }
  #arBox.on { visibility:visible; }

  #arFallback {
    position:absolute; top:0; left:0;
    width:100%; height:100%;
    display:flex; align-items:center; justify-content:center;
    background:linear-gradient(135deg,#00ff88,#ff2d95);
    color:#fff; font-size:16px; font-weight:900;
    font-family:monospace; letter-spacing:2px;
    text-shadow:0 2px 12px rgba(0,0,0,.6);
    z-index:1;
  }

  #arVideo {
    position:absolute; top:0; left:0;
    width:100%; height:100%;
    object-fit:cover;
    display:block;
    background:transparent;
    z-index:2;
  }

  #overlay { position:fixed; top:0; left:0; right:0; bottom:0;
    width:100%; height:100%; z-index:3; pointer-events:none; }

  #ver { position:fixed; right:6px; bottom:6px;
    font-family:monospace; font-size:10px;
    color:rgba(255,255,255,.35);
    z-index:600; pointer-events:none; }
</style>
</head>
<body>

<div id="hud">
  <div id="hudText">Запуск…</div>
  <div id="fps">-- FPS</div>
</div>

<div id="tapper"><div id="visual">ТАПНИ ЧТОБЫ ЗАПУСТИТЬ AR</div></div>

<canvas id="overlay"></canvas>

<div id="arBox">
  <div id="arFallback">VIDEO</div>
  <video id="arVideo" muted loop playsinline webkit-playsinline preload="auto"></video>
</div>

<div id="ver">v21</div>

<script>
(function () {
  "use strict";

  var hud        = document.getElementById("hud");
  var hudText    = document.getElementById("hudText");
  var fpsEl      = document.getElementById("fps");
  var tapper     = document.getElementById("tapper");
  var visual     = document.getElementById("visual");
  var overlay    = document.getElementById("overlay");
  var ctx        = overlay.getContext("2d");
  var arBox      = document.getElementById("arBox");
  var arVideo    = document.getElementById("arVideo");
  var arFallback = document.getElementById("arFallback");

  // ═══════════════════════════════════════════════════════════════
  // ИСТОЧНИК AR-ВИДЕО
  // Залей video.mp4 рядом с этим файлом.
  // Совет: 480p, H.264, не длиннее 30 секунд — так iPhone 8 не будет тормозить.
  // ═══════════════════════════════════════════════════════════════
  var AR_VIDEO_SRC = "video.mp4";

  // ═══════════════════════════════════════════════════════════════
  // НАСТРОЙКИ ДЕТЕКТОРА
  // ═══════════════════════════════════════════════════════════════
  var PROC_W         = 140;
  var MIN_PIXELS     = 20;
  var EDGE_DENSITY   = 0.40;

  // Сглаживание рамки: 0 = не двигается, 1 = мгновенно догоняет.
  // 0.85 — почти намертво, шлейфа нет.
  var SMOOTH_K       = 0.85;

  // Сколько кадров держать рамку, если метку не нашли.
  // Меньше = быстрее спрячется старая рамка = меньше «призраков».
  var MISS_TOLERANCE = 3;

  var HUE_MIN = 290, HUE_MAX = 360;
  var SAT_MIN = 0.30, VAL_MIN = 70;

  var CONTENT_W = 100;
  var CONTENT_H = 100;

  function setHud(text, isHit) {
    hudText.textContent = text;
    if (isHit) {
      hud.classList.add("hit");
    } else {
      hud.classList.remove("hit");
    }
  }

  // ═══════════════════════════════════════════════════════════════
  // FPS
  // ═══════════════════════════════════════════════════════════════
  var fpsFrames = [];
  var fpsLastUpdate = 0;
  function tickFps() {
    var now = performance.now();
    fpsFrames.push(now);
    while (fpsFrames.length && (now - 1000) > fpsFrames[0]) fpsFrames.shift();
    if ((now - fpsLastUpdate) > 400) {
      fpsLastUpdate = now;
      var f = fpsFrames.length;
      fpsEl.textContent = f + " FPS";
      if (24 > f) fpsEl.className = "low";
      else if (45 > f) fpsEl.className = "mid";
      else fpsEl.className = "";
    }
  }

  // ═══════════════════════════════════════════════════════════════
  // РАЗМЕРЫ
  // ═══════════════════════════════════════════════════════════════
  var screenW = 0, screenH = 0;
  function updateScreenSize() {
    screenW = window.innerWidth;
    screenH = window.innerHeight;
    var dpr = Math.min(window.devicePixelRatio || 1, 2);
    overlay.width  = Math.round(screenW * dpr);
    overlay.height = Math.round(screenH * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  window.addEventListener("resize", updateScreenSize);
  window.addEventListener("orientationchange", function () {
    setTimeout(updateScreenSize, 200);
  });

  // ═══════════════════════════════════════════════════════════════
  // HSV: розовый?
  // ═══════════════════════════════════════════════════════════════
  function isPink(r, g, b) {
    var max = r > g ? (r > b ? r : b) : (g > b ? g : b);
    if (VAL_MIN > max) return false;
    var min = r > g ? (g > b ? b : g) : (r > b ? b : r);
    var d = max - min;
    if (d === 0) return false;
    if (SAT_MIN > (d / max)) return false;
    var h;
    if (max === r)      h = ((g - b) / d) % 6;
    else if (max === g) h = (b - r) / d + 2;
    else                h = (r - g) / d + 4;
    h = h * 60;
    if (0 > h) h = h + 360;
    return (h >= HUE_MIN) && (HUE_MAX >= h);
  }

  // ═══════════════════════════════════════════════════════════════
  // СОСТОЯНИЕ
  // ═══════════════════════════════════════════════════════════════
  var cameraVideo = null;
  var running = false;
  var tapped = false;

  var proc = document.createElement("canvas");
  var pctx = proc.getContext("2d", { willReadFrequently: true });

  var maskA = null, maskB = null, labelsBuf = null, stackBuf = null;
  var colCount = null, rowCount = null;

  var rawBox = null, smoothBox = null;
  var missCount = 0;

  // ═══════════════════════════════════════════════════════════════
  // ДЕТЕКТОР
  // ═══════════════════════════════════════════════════════════════
  function detectBox(data, W, H) {
    var total = W * H;
    var i, p;

    for (i = 0, p = 0; total > p; i += 4, p++) {
      maskA[p] = isPink(data[i], data[i + 1], data[i + 2]) ? 1 : 0;
    }

    var y, x;
    for (y = 1; (H - 1) > y; y++) {
      var ro = y * W;
      for (x = 1; (W - 1) > x; x++) {
        var idx = ro + x;
        var ok = maskA[idx] && maskA[idx - 1] && maskA[idx + 1] &&
                 maskA[idx - W] && maskA[idx + W];
        maskB[idx] = ok ? 1 : 0;
      }
    }
    var xx, yy;
    for (xx = 0; W > xx; xx++) { maskB[xx] = 0; maskB[total - W + xx] = 0; }
    for (yy = 0; H > yy; yy++) { maskB[yy * W] = 0; maskB[yy * W + W - 1] = 0; }

    for (p = 0; total > p; p++) labelsBuf[p] = -1;
    var bestId = -1, bestSize = 0, blobCount = 0;

    var start;
    for (start = 0; total > start; start++) {
      if (maskB[start] !== 1 || labelsBuf[start] !== -1) continue;
      var id = blobCount++;
      var count = 0, sp = 0;
      stackBuf[sp++] = start;
      labelsBuf[start] = id;

      while (sp > 0) {
        p = stackBuf[--sp];
        count++;
        var px = p % W;
        var py = (p - px) / W;
        var x0 = px > 0 ? px - 1 : 0;
        var x1 = (W - 1) > px ? px + 1 : W - 1;
        var y0 = py > 0 ? py - 1 : 0;
        var y1 = (H - 1) > py ? py + 1 : H - 1;
        var cy, cx;
        for (cy = y0; y1 >= cy; cy++) {
          var r2 = cy * W;
          for (cx = x0; x1 >= cx; cx++) {
            var q = r2 + cx;
            if (maskB[q] === 1 && labelsBuf[q] === -1) {
              labelsBuf[q] = id;
              stackBuf[sp++] = q;
            }
          }
        }
      }
      if (count > bestSize) { bestSize = count; bestId = id; }
    }

    if (bestId === -1 || MIN_PIXELS > bestSize) return null;

    var c, r;
    for (c = 0; W > c; c++) colCount[c] = 0;
    for (r = 0; H > r; r++) rowCount[r] = 0;

    var maxCol = 0, maxRow = 0;
    for (p = 0; total > p; p++) {
      if (labelsBuf[p] !== bestId) continue;
      var gx = p % W;
      var gy = (p - gx) / W;
      var cc = ++colCount[gx];
      if (cc > maxCol) maxCol = cc;
      var rc = ++rowCount[gy];
      if (rc > maxRow) maxRow = rc;
    }

    var colTh = Math.max(2, Math.floor(maxCol * EDGE_DENSITY));
    var rowTh = Math.max(2, Math.floor(maxRow * EDGE_DENSITY));

    var tx0 = 0, tx1 = W - 1, ty0 = 0, ty1 = H - 1;
    var a, a2, b, b2;
    for (a = 0; W > a; a++)         if (colCount[a] >= colTh) { tx0 = a; break; }
    for (a2 = W - 1; a2 >= 0; a2--) if (colCount[a2] >= colTh) { tx1 = a2; break; }
    for (b = 0; H > b; b++)         if (rowCount[b] >= rowTh) { ty0 = b; break; }
    for (b2 = H - 1; b2 >= 0; b2--) if (rowCount[b2] >= rowTh) { ty1 = b2; break; }

    return { x: tx0, y: ty0, w: tx1 - tx0 + 1, h: ty1 - ty0 + 1 };
  }

  function procToScreen(b) {
    var vw = cameraVideo.videoWidth  || 640;
    var vh = cameraVideo.videoHeight || 480;
    var scale = Math.max(screenW / vw, screenH / vh);
    var offX = (screenW - vw * scale) / 2;
    var offY = (screenH - vh * scale) / 2;
    var kx = (vw / proc.width)  * scale;
    var ky = (vh / proc.height) * scale;

    return {
      x: offX + b.x * kx,
      y: offY + b.y * ky,
      w: b.w * kx,
      h: b.h * ky
    };
  }

  // ── Гомография: единичный квадрат → 4 точки ──────────────────
  function unitSquareToQuad(p) {
    var x0 = p[0], y0 = p[1], x1 = p[2], y1 = p[3];
    var x2 = p[4], y2 = p[5], x3 = p[6], y3 = p[7];
    var dx1 = x1 - x2, dx2 = x3 - x2, dx3 = x0 - x1 + x2 - x3;
    var dy1 = y1 - y2, dy2 = y3 - y2, dy3 = y0 - y1 + y2 - y3;
    var den = dx1 * dy2 - dx2 * dy1;
    var g = 0, h = 0;
    if ((Math.abs(dx3) > 1e-9) || (Math.abs(dy3) > 1e-9)) {
      if (1e-9 > Math.abs(den)) return null;
      g = (dx3 * dy2 - dx2 * dy3) / den;
      h = (dx1 * dy3 - dx3 * dy1) / den;
    }
    return {
      a: x1 - x0 + g * x1, b: x3 - x0 + h * x3, c: x0,
      d: y1 - y0 + g * y1, e: y3 - y0 + h * y3, f: y0,
      g: g, h: h
    };
  }

  var lastTransform = "";
  function applyCornersToBox(box) {
    var tl = { x: box.x,         y: box.y };
    var tr = { x: box.x + box.w, y: box.y };
    var br = { x: box.x + box.w, y: box.y + box.h };
    var bl = { x: box.x,         y: box.y + box.h };

    var H = unitSquareToQuad([tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y]);
    if (!H) return;

    var CW = CONTENT_W;
    var CH = CONTENT_H;

    var m11 = H.a / CW;
    var m12 = H.d / CW;
    var m13 = 0;
    var m14 = H.g / CW;

    var m21 = H.b / CH;
    var m22 = H.e / CH;
    var m23 = 0;
    var m24 = H.h / CH;

    var m31 = 0;
    var m32 = 0;
    var m33 = 1;
    var m34 = 0;

    var m41 = H.c;
    var m42 = H.f;
    var m43 = 0;
    var m44 = 1;

    var t = "matrix3d(" +
      m11 + "," + m12 + "," + m13 + "," + m14 + "," +
      m21 + "," + m22 + "," + m23 + "," + m24 + "," +
      m31 + "," + m32 + "," + m33 + "," + m34 + "," +
      m41 + "," + m42 + "," + m43 + "," + m44 +
    ")";

    if (t !== lastTransform) {
      arBox.style.transform = t;
      arBox.style.webkitTransform = t;
      lastTransform = t;
    }
  }

  function drawBox(b) {
    if ((4 > b.w) || (4 > b.h)) return;

    ctx.strokeStyle = "rgba(255,45,149,.35)";
    ctx.lineWidth = 8;
    ctx.strokeRect(b.x, b.y, b.w, b.h);

    ctx.strokeStyle = "#ff2d95";
    ctx.lineWidth = 3;
    ctx.strokeRect(b.x, b.y, b.w, b.h);

    ctx.strokeStyle = "rgba(255,255,255,.9)";
    ctx.lineWidth = 1;
    ctx.strokeRect(b.x + 0.5, b.y + 0.5, b.w - 1, b.h - 1);

    var c = Math.min(18, b.w * 0.25, b.h * 0.25);
    ctx.strokeStyle = "#fff";
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(b.x + c, b.y);
    ctx.lineTo(b.x, b.y);
    ctx.lineTo(b.x, b.y + c);
    ctx.moveTo(b.x + b.w - c, b.y);
    ctx.lineTo(b.x + b.w, b.y);
    ctx.lineTo(b.x + b.w, b.y + c);
    ctx.moveTo(b.x + c, b.y + b.h);
    ctx.lineTo(b.x, b.y + b.h);
    ctx.lineTo(b.x, b.y + b.h - c);
    ctx.moveTo(b.x + b.w - c, b.y + b.h);
    ctx.lineTo(b.x + b.w, b.y + b.h);
    ctx.lineTo(b.x + b.w, b.y + b.h - c);
    ctx.stroke();
  }

  function playArVideo() {
    if (!arVideo.paused && (arVideo.readyState >= 2)) return;
    var p = arVideo.play();
    if (p && p.catch) p.catch(function () {});
  }

  // ═══════════════════════════════════════════════════════════════
  // ГЛАВНЫЙ ЦИКЛ
  // ═══════════════════════════════════════════════════════════════
  function loop() {
    if (!running) return;
    requestAnimationFrame(loop);

    if (document.hidden) return;
    if (!cameraVideo || (2 > cameraVideo.readyState)) return;

    tickFps();

    // детекция
    var box = null;
    try {
      pctx.drawImage(cameraVideo, 0, 0, proc.width, proc.height);
      var img = pctx.getImageData(0, 0, proc.width, proc.height);
      box = detectBox(img.data, proc.width, proc.height);
    } catch (e) {}

    if (box) {
      rawBox = box;
      missCount = 0;
    } else {
      missCount++;
      if (missCount > MISS_TOLERANCE) {
        rawBox = null;
        smoothBox = null;
      }
    }

    // сглаживание
    if (rawBox) {
      if (!smoothBox) {
        smoothBox = { x: rawBox.x, y: rawBox.y, w: rawBox.w, h: rawBox.h };
      } else {
        smoothBox.x += (rawBox.x - smoothBox.x) * SMOOTH_K;
        smoothBox.y += (rawBox.y - smoothBox.y) * SMOOTH_K;
        smoothBox.w += (rawBox.w - smoothBox.w) * SMOOTH_K;
        smoothBox.h += (rawBox.h - smoothBox.h) * SMOOTH_K;
      }
    }

    // рисуем рамку
    ctx.clearRect(0, 0, overlay.width, overlay.height);

    if (smoothBox) {
      var s = procToScreen(smoothBox);
      drawBox(s);
      applyCornersToBox(s);

      if (!arBox.classList.contains("on")) {
        arBox.classList.add("on");
        playArVideo();
      }

      setHud("НАЙДЕН!  " + Math.round(s.w) + "x" + Math.round(s.h) + " px", true);
    } else {
      if (arBox.classList.contains("on")) {
        arBox.classList.remove("on");
      }
      setHud("Ищу розовый…", false);
    }
  }

  // ═══════════════════════════════════════════════════════════════
  // СТАРТ
  // ═══════════════════════════════════════════════════════════════
  function startAR() {
    updateScreenSize();

    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
      setHud("НЕТ ДОСТУПА К КАМЕРЕ");
      visual.textContent = "НЕТ ДОСТУПА";
      return;
    }

    cameraVideo = document.createElement("video");
    cameraVideo.setAttribute("playsinline", "");
    cameraVideo.setAttribute("webkit-playsinline", "");
    cameraVideo.muted = true;
    cameraVideo.autoplay = true;
    cameraVideo.style.cssText =
      "position:fixed;top:0;left:0;width:100%;height:100%;" +
      "object-fit:cover;background:#000;z-index:1";

    navigator.mediaDevices.getUserMedia({
      audio: false,
      video: { facingMode: "environment" }
    }).then(function (stream) {
      cameraVideo.srcObject = stream;
      document.body.appendChild(cameraVideo);

      var playP = cameraVideo.play();
      if (playP && playP.catch) playP.catch(function () {});

      tapper.style.display = "none";
      setHud("Камера включается…");

      var gotReady = false;
      var attempts = 0;

      function tryStart() {
        if (gotReady) return;
        var vw = cameraVideo.videoWidth;
        var vh = cameraVideo.videoHeight;
        if ((vw > 0) && (vh > 0)) {
          gotReady = true;
          onCameraReady(vw, vh);
        }
      }

      cameraVideo.addEventListener("loadedmetadata", tryStart);
      cameraVideo.addEventListener("playing", tryStart);
      cameraVideo.addEventListener("canplay", tryStart);

      var poll = setInterval(function () {
        attempts++;
        tryStart();
        if (gotReady || (attempts > 40)) clearInterval(poll);
      }, 150);

      tryStart();

    }).catch(function (err) {
      var name = (err && err.name) || "?";
      setHud("ОШИБКА КАМЕРЫ: " + name);
      visual.textContent = "ОШИБКА: " + name;
    });
  }

  function onCameraReady(vw, vh) {
    if (running) return;
    running = true;

    proc.width = PROC_W;
    proc.height = Math.max(50, Math.round(PROC_W * vh / vw));

    var total = proc.width * proc.height;
    maskA     = new Uint8Array(total);
    maskB     = new Uint8Array(total);
    labelsBuf = new Int32Array(total);
    stackBuf  = new Int32Array(total);
    colCount  = new Int32Array(proc.width);
    rowCount  = new Int32Array(proc.height);

    arVideo.src = AR_VIDEO_SRC;
    arVideo.load();
    try { arVideo.play().catch(function () {}); } catch (e) {}

    setHud("Ищу розовый…");
    requestAnimationFrame(loop);
  }

  function onTap() {
    if (tapped) return;
    tapped = true;
    visual.textContent = "ЗАПУСК…";
    startAR();
  }

  document.addEventListener("touchstart", onTap, true);
  document.addEventListener("pointerdown", onTap, true);
  document.addEventListener("click", onTap, true);

  window.addEventListener("load", updateScreenSize);

  arVideo.addEventListener("pause", function () {
    if (running && arBox.classList.contains("on")) {
      setTimeout(playArVideo, 100);
    }
  });
  arVideo.addEventListener("ended", function () {
    if (running) { arVideo.currentTime = 0; playArVideo(); }
  });
  arVideo.addEventListener("error", function () {
    var c = arVideo.error ? arVideo.error.code : "?";
    setHud("Видео ошибка " + c + " — проверь video.mp4", false);
  });
  arVideo.addEventListener("loadeddata", function () {
    arFallback.style.display = "none";
  });

  updateScreenSize();
  setHud("Готов к запуску");

})();
</script>
</body>
</html>