Загрузка данных
<!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>