/* .v1 — платформа разметки мультимодальных данных для VLA / гуманоидов — dark theme */ const { useState, useEffect, useRef } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "#EA0A23", "cursor": true, "loader": true } /*EDITMODE-END*/; /* ============== HOOKS ============== */ function useReveal(threshold = 0.15) { const ref = useRef(null); useEffect(() => { const el = ref.current;if (!el) return; const io = new IntersectionObserver((entries) => { entries.forEach((e) => {if (e.isIntersecting) {el.classList.add("in");io.disconnect();}}); }, { threshold }); io.observe(el); return () => io.disconnect(); }, [threshold]); return ref; } function useScrollY() { const [y, setY] = useState(0); useEffect(() => { const on = () => setY(window.scrollY); on();window.addEventListener("scroll", on, { passive: true }); return () => window.removeEventListener("scroll", on); }, []); return y; } function useClock() { const [t, setT] = useState(""); useEffect(() => { const fmt = () => { try {setT(new Intl.DateTimeFormat("ru-RU", { hour: "2-digit", minute: "2-digit", timeZone: "Europe/Moscow", hour12: false }).format(new Date()));} catch {setT("");} }; fmt();const id = setInterval(fmt, 30000); return () => clearInterval(id); }, []); return t; } /* ============== PRIMITIVES ============== */ function getText(node) { if (node == null || typeof node === "boolean") return ""; if (typeof node === "string" || typeof node === "number") return String(node); if (Array.isArray(node)) return node.map(getText).join(""); if (React.isValidElement(node)) return getText(node.props.children); return ""; } function Words({ children, delay = 0 }) { const ref = useRef(null); useEffect(() => { const el = ref.current;if (!el) return; const words = el.querySelectorAll(".word"); const fire = () => words.forEach((w, i) => setTimeout(() => w.classList.add("in"), delay + i * 70)); const r = el.getBoundingClientRect(); if (r.top < innerHeight && r.bottom > 0) {fire();} else { const io = new IntersectionObserver((entries) => { entries.forEach((e) => {if (e.isIntersecting) {fire();io.disconnect();}}); }, { threshold: 0 }); io.observe(el); // safety: force-show after 2.5s no matter what const safety = setTimeout(() => {words.forEach((w) => w.classList.add("in"));io.disconnect();}, 2500); return () => {io.disconnect();clearTimeout(safety);}; } // safety for in-viewport too const safety = setTimeout(() => words.forEach((w) => w.classList.add("in")), 2500); return () => clearTimeout(safety); }, [delay]); const parts = getText(children).split(" "); return ( {parts.map((w, i) => {w} {i < parts.length - 1 ? " " : ""} )} ); } function SectionNum({ label, num, total = "07" }) { return (
{label} {num} | {total}
); } function Bracket({ children, accent = false }) { return ( [ {children} ] ); } /* ============== TYPEWRITER LINES — types each bracket word in sequence, then loops ============== */ function TypeLines({ words }) { const [shown, setShown] = useState(() => words.map(() => "")); const [revealedMax, setRevealedMax] = useState(0); const [caretLine, setCaretLine] = useState(0); useEffect(() => { let timer,mounted = true; let line = 0,pos = 0; const typeChar = () => { if (!mounted) return; const full = words[line]; pos++; setShown((s) => {const n = [...s];n[line] = full.slice(0, pos);return n;}); if (pos >= full.length) { if (line < words.length - 1) { // brief pause, then drop to next line and keep typing timer = setTimeout(() => { if (!mounted) return; line++;pos = 0; setRevealedMax(line);setCaretLine(line); typeChar(); }, 440); } else { setCaretLine(-1); // all three typed — hold, hide caret timer = setTimeout(restart, 2400); } } else { timer = setTimeout(typeChar, 78 + Math.random() * 72); } }; const restart = () => { if (!mounted) return; line = 0;pos = 0; setShown(words.map(() => "")); setRevealedMax(0);setCaretLine(0); timer = setTimeout(typeChar, 460); }; timer = setTimeout(typeChar, 700); return () => {mounted = false;clearTimeout(timer);}; }, []); return words.map((w, i) => [ {shown[i]} {caretLine === i ? {i < words.length - 1 ?
: null}
); } /* ============== DECORATIVE PRIMITIVES (generic graphic techniques) ============== */ function HalftoneRing({ size = 260, color = "rgba(255,255,255,0.95)" }) { const cx = size / 2,cy = size / 2; const rings = [ { r: size * 0.20, n: 12, d: 1.6 }, { r: size * 0.27, n: 22, d: 1.9 }, { r: size * 0.34, n: 32, d: 2.2 }, { r: size * 0.40, n: 42, d: 2.4 }, { r: size * 0.46, n: 28, d: 1.8 } // sparser outer ]; const dots = []; rings.forEach((ring, ri) => { for (let i = 0; i < ring.n; i++) { const a = i / ring.n * Math.PI * 2 + ri * 0.12; dots.push(); } }); return {dots}; } function DotCluster({ size = 90, color = "var(--accent)" }) { return ( ); } /* ============== HERO BACKDROP — dotted concentric orbits (programmatic SVG) ============== */ function HeroBackdrop({ size = 720 }) { const cx = size / 2,cy = size / 2; const rings = []; const ringCount = 28; for (let i = 0; i < ringCount; i++) { const t = i / (ringCount - 1); const r = 90 + t * (size * 0.48 - 90); const density = 1 - Math.pow(t, 1.4) * 0.85; const n = Math.max(8, Math.round(r * 0.38 * density)); const dotR = 1.6 - t * 1.0; const opacity = 0.18 + (1 - t) * 0.42; rings.push({ r, n, dotR, opacity }); } const dots = []; rings.forEach((ring, ri) => { const phase = ri * 0.13; for (let i = 0; i < ring.n; i++) { const a = i / ring.n * Math.PI * 2 + phase; dots.push( ); } }); return ( {dots} ); } /* ============== HERO SPHERE — canvas particle sphere ============== */ function HeroSphere({ size = 380 }) { const ref = useRef(null); useEffect(() => { const canvas = ref.current;if (!canvas) return; const dpr = Math.min(2, window.devicePixelRatio || 1); canvas.width = size * dpr;canvas.height = size * dpr; canvas.style.width = size + "px";canvas.style.height = size + "px"; const ctx = canvas.getContext("2d"); ctx.scale(dpr, dpr); // Fibonacci-sphere uniform distribution const N = 720; const pts = []; const golden = Math.PI * (3 - Math.sqrt(5)); for (let i = 0; i < N; i++) { const y = 1 - i / (N - 1) * 2; const r = Math.sqrt(1 - y * y); const theta = golden * i; pts.push({ x: Math.cos(theta) * r, y, z: Math.sin(theta) * r, accent: Math.random() < 0.045, sz: 0.7 + Math.random() * 0.6 }); } let mx = 0,my = 0,tx = 0,ty = 0; const onMove = (e) => { const rect = canvas.getBoundingClientRect(); const ccx = rect.left + rect.width / 2; const ccy = rect.top + rect.height / 2; tx = (e.clientX - ccx) / window.innerWidth * 0.55; ty = (e.clientY - ccy) / window.innerHeight * 0.4; }; window.addEventListener("pointermove", onMove); let raf,t = 0,stopped = false; const cx = size / 2,cy = size / 2; const R = size * 0.42; const accentRgb = (() => { const v = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#EA0A23"; const hex = v.replace("#", ""); return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)]; })(); // ===== annotation bounding box (data-labeling motif) ===== const classes = ["obj", "kp", "grasp", "edge", "node", "pose"]; let targetIdx = 0,holdUntil = 0; let boxCX = cx,boxCY = cy,boxHS = 64,lockT = 0; let conf = 0.95,label = "obj 0.95"; // ===== letter flock — "Базис / Консенсус / Контур" ===== const FWORDS = ["Базис", "Консенсус", "Контур"]; const wordFontPx = Math.max(20, Math.round(size * 0.048)); const flockFontPx = Math.max(12, Math.round(size * 0.030)); ctx.font = `700 ${wordFontPx}px Onest, system-ui, sans-serif`; const layouts = FWORDS.map((w) => { const chars = Array.from(w); const widths = chars.map((c) => ctx.measureText(c).width); const trk = wordFontPx * 0.04; const total = widths.reduce((a, b) => a + b, 0) + trk * (chars.length - 1); let xx = cx - total / 2; const slots = chars.map((c, i) => { const s = { x: xx + widths[i] / 2, y: cy }; xx += widths[i] + trk; return s; }); return { chars, slots }; }); ctx.textAlign = "start";ctx.textBaseline = "alphabetic"; const flockBaseX = size * 0.115,flockBaseY = size * 0.090; const flockSpread = size * 0.045; const flock = []; FWORDS.forEach((w, wi) => { Array.from(w).forEach((ch, si) => { flock.push({ ch, wi, si, oang: Math.random() * Math.PI * 2, orad: (0.30 + Math.sqrt(Math.random()) * 0.70) * flockSpread, ospd: 1.30 + Math.random() * 1.00, wph: Math.random() * Math.PI * 2, wsp: 0.5 + Math.random() * 0.7, wamp: size * 0.006 + Math.random() * size * 0.012, ddx: Math.random() * 2 - 1, x: flockBaseX, y: flockBaseY, s: flockFontPx, a: 0, sparkCycle: -1 }); }); }); const F_IN = 1300,F_HOLD = 1500,F_OUT = 650,F_GAP = 350; const F_CYCLE = F_IN + F_HOLD + F_OUT + F_GAP; const F_STAG = 80,F_TRAVEL = 520; const flockStart = performance.now(); let sparks = []; const shOff = document.createElement("canvas"); const shCtx = shOff.getContext("2d"); const shatterLetter = (p) => { const fs = Math.max(8, p.s); const pad = 4; shCtx.font = `700 ${fs}px Onest, system-ui, sans-serif`; const w = Math.ceil(shCtx.measureText(p.ch).width) + pad * 2; const h = Math.ceil(fs * 1.45) + pad * 2; shOff.width = w;shOff.height = h; shCtx.font = `700 ${fs}px Onest, system-ui, sans-serif`; shCtx.textAlign = "center";shCtx.textBaseline = "middle"; shCtx.clearRect(0, 0, w, h); shCtx.fillStyle = "#0A0A0A"; shCtx.fillText(p.ch, w / 2, h / 2); const data = shCtx.getImageData(0, 0, w, h).data; const step = 2; const ox = p.x - w / 2,oy = p.y - h / 2; for (let yy = 0; yy < h; yy += step) { for (let xx = 0; xx < w; xx += step) { if (data[(yy * w + xx) * 4 + 3] > 60) { const dx = (xx - w / 2) / w,dy = (yy - h / 2) / h; sparks.push({ x: ox + xx + Math.random() * step, y: oy + yy + Math.random() * step, vx: dx * size * 0.012 + (Math.random() - 0.5) * size * 0.006, vy: dy * size * 0.006 - size * 0.004 - Math.random() * size * 0.005, life: 24 + Math.random() * 34, max: 58, sz: 0.8 + Math.random() * 1.0 }); } } } }; const clampF = (v, a, b) => v < a ? a : v > b ? b : v; const eoCubic = (p) => 1 - Math.pow(1 - p, 3); const render = () => { if (stopped) return; t += 0.0035; mx += (tx - mx) * 0.06; my += (ty - my) * 0.06; ctx.clearRect(0, 0, size, size); const yaw = t + mx * 0.9; const pitch = 0.14 + my * 0.7; const cy_ = Math.cos(yaw),sy_ = Math.sin(yaw); const cp_ = Math.cos(pitch),sp_ = Math.sin(pitch); const breath = 1 + Math.sin(t * 1.2) * 0.018; const screen = pts.map((p) => { const x1 = p.x * cy_ + p.z * sy_; const z1 = -p.x * sy_ + p.z * cy_; const y1 = p.y * cp_ - z1 * sp_; const z2 = p.y * sp_ + z1 * cp_; return { x: x1, y: y1, z: z2, accent: p.accent, sz: p.sz }; }); screen.sort((a, b) => a.z - b.z); // back-glow behind sphere const grd = ctx.createRadialGradient(cx, cy, 0, cx, cy, R * 0.95); grd.addColorStop(0, `rgba(${accentRgb[0]},${accentRgb[1]},${accentRgb[2]},0.18)`); grd.addColorStop(1, `rgba(${accentRgb[0]},${accentRgb[1]},${accentRgb[2]},0)`); ctx.fillStyle = grd; ctx.beginPath();ctx.arc(cx, cy, R * 0.95, 0, Math.PI * 2);ctx.fill(); for (const p of screen) { const depth = (p.z + 1) / 2; // 0 back, 1 front const px = cx + p.x * R * breath; const py = cy + p.y * R * breath; const r = (p.sz + depth * 1.4) * (p.accent ? 1.6 : 1); const op = p.accent ? 0.6 + depth * 0.4 : 0.10 + depth * 0.78; ctx.fillStyle = p.accent ? `rgba(${accentRgb[0]},${accentRgb[1]},${accentRgb[2]},${op})` : `rgba(10,10,10,${op})`; ctx.beginPath(); ctx.arc(px, py, r, 0, Math.PI * 2); ctx.fill(); } // ===== annotation bounding box — acquire / track / re-target ===== const project = (p) => { const x1 = p.x * cy_ + p.z * sy_; const z1 = -p.x * sy_ + p.z * cy_; const y1 = p.y * cp_ - z1 * sp_; const z2 = p.y * sp_ + z1 * cp_; return { x: x1, y: y1, z: z2 }; }; const nowMs = performance.now(); if (nowMs > holdUntil) { // gather front-facing points sitting away from the silhouette edge const cands = []; for (let i = 0; i < pts.length; i++) { const pr = project(pts[i]); const rad = Math.hypot(pr.x, pr.y); if (pr.z > 0.5 && rad < 0.78 && (pts[i].accent || Math.random() < 0.05)) cands.push(i); } if (cands.length) { let pick = cands[Math.random() * cands.length | 0]; if (pick === targetIdx && cands.length > 1) pick = cands[Math.random() * cands.length | 0]; targetIdx = pick; } holdUntil = nowMs + 1300 + Math.random() * 800; boxHS = 72; // snap open, then contract = "acquire target" lockT = 0; conf = 0.9 + Math.random() * 0.098; label = classes[Math.random() * classes.length | 0] + " " + conf.toFixed(2); } // track the locked point as the sphere keeps rotating const tp = project(pts[targetIdx]); const tdepth = (tp.z + 1) / 2; const tpx = cx + tp.x * R * breath; const tpy = cy + tp.y * R * breath; const tightHS = 19 + tdepth * 12; boxCX += (tpx - boxCX) * 0.2; boxCY += (tpy - boxCY) * 0.2; boxHS += (tightHS - boxHS) * 0.16; lockT += (1 - lockT) * 0.12; // fade out as the tracked point rotates toward the back const boxVis = Math.max(0, Math.min(1, (tp.z - 0.12) / 0.32)); if (boxVis > 0.01) { const bx = boxCX,by = boxCY,hs = boxHS; const aC = `${accentRgb[0]},${accentRgb[1]},${accentRgb[2]}`; ctx.save(); ctx.lineJoin = "round"; // faint full frame ctx.strokeStyle = `rgba(${aC},${(0.20 + 0.38 * lockT) * boxVis})`; ctx.lineWidth = 1; ctx.strokeRect(bx - hs, by - hs, hs * 2, hs * 2); // L-shaped corner brackets const cl = 7 + 5 * lockT; ctx.strokeStyle = `rgba(${aC},${0.95 * boxVis})`; ctx.lineWidth = 2; ctx.lineCap = "round"; const corner = (sx, sy, dx, dy) => { ctx.beginPath(); ctx.moveTo(sx + dx * cl, sy); ctx.lineTo(sx, sy); ctx.lineTo(sx, sy + dy * cl); ctx.stroke(); }; corner(bx - hs, by - hs, 1, 1); corner(bx + hs, by - hs, -1, 1); corner(bx - hs, by + hs, 1, -1); corner(bx + hs, by + hs, -1, -1); // center crosshair ctx.strokeStyle = `rgba(${aC},${0.7 * boxVis})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(bx - 4, by);ctx.lineTo(bx + 4, by); ctx.moveTo(bx, by - 4);ctx.lineTo(bx, by + 4); ctx.stroke(); // label tag ctx.font = "600 9px 'JetBrains Mono', ui-monospace, monospace"; const tw = ctx.measureText(label).width; const tagH = 13,tagW = tw + 10; let tagX = bx - hs,tagY = by - hs - tagH - 2; if (tagY < 2) tagY = by + hs + 2; ctx.globalAlpha = boxVis * (0.45 + 0.55 * lockT); ctx.fillStyle = `rgb(${aC})`; ctx.fillRect(tagX, tagY, tagW, tagH); ctx.fillStyle = "#fff"; ctx.textBaseline = "middle"; ctx.fillText(label, tagX + 5, tagY + tagH / 2 + 0.5); ctx.restore(); } // ===== letter flock: drift → staggered fly-in → hold → dissolve → loop ===== const fnow = performance.now() - flockStart; const cycleIdx = Math.floor(fnow / F_CYCLE); const fslot = cycleIdx % 3; const localT = fnow % F_CYCLE; const fcx = flockBaseX + Math.sin(t * 0.5) * size * 0.016; const fcy = flockBaseY + Math.cos(t * 0.42) * size * 0.014; for (const p of flock) { const active = p.wi === fslot; const oa = p.oang + t * p.ospd; const rr = p.orad + Math.sin(t * p.wsp + p.wph) * p.wamp; const hx = fcx + Math.cos(oa) * rr + Math.cos(t * p.wsp * 1.3 + p.wph) * p.wamp * 0.4; const hy = fcy + Math.sin(oa) * rr * 0.58 + Math.sin(t * p.wsp * 1.1 + p.wph) * p.wamp * 0.4; let gx = hx,gy = hy,ga = 0.42,gs = flockFontPx,posLerp = 0.045,aLerp = 0.16; if (active) { const sl = layouts[p.wi].slots[p.si]; const tHoldEnd = F_IN + F_HOLD; const tOutEnd = tHoldEnd + F_OUT; if (localT < F_IN) { // fly out of the flock one by one, fast, toward the word slot const pr = clampF((localT - p.si * F_STAG) / F_TRAVEL, 0, 1); const e = eoCubic(pr); gx = hx + (sl.x - hx) * e; gy = hy + (sl.y - hy) * e; gs = flockFontPx + (wordFontPx - flockFontPx) * e; ga = 0.42 + 0.58 * clampF(pr * 1.5, 0, 1); posLerp = 0.5;aLerp = 0.3; } else if (localT < tHoldEnd) { // word formed, held in the sphere centre gx = sl.x;gy = sl.y;gs = wordFontPx;ga = 1;posLerp = 0.45;aLerp = 0.4; } else if (localT < tOutEnd) { // dissolve (Telegram-style): glyph shatters into tiny pixels that scatter & fade if (p.sparkCycle !== cycleIdx) { p.sparkCycle = cycleIdx; shatterLetter(p); p.a = 0; } gx = sl.x;gy = sl.y;gs = p.s;ga = 0;posLerp = 0;aLerp = 1; } else { // gap — slip back toward the flock, invisible gx = hx;gy = hy;gs = flockFontPx;ga = 0;posLerp = 0.1;aLerp = 0.25; } } p.x += (gx - p.x) * posLerp; p.y += (gy - p.y) * posLerp; p.s += (gs - p.s) * posLerp; p.a += (ga - p.a) * aLerp; if (p.a > 0.015) { const f = clampF((p.s - flockFontPx) / (wordFontPx - flockFontPx), 0, 1); const cr = Math.round(107 + (10 - 107) * f); const cg = Math.round(107 + (10 - 107) * f); const cb = Math.round(107 + (10 - 107) * f); ctx.save(); ctx.globalAlpha = p.a; ctx.textAlign = "center";ctx.textBaseline = "middle"; ctx.font = `700 ${p.s.toFixed(1)}px Onest, system-ui, sans-serif`; if (f > 0.25) {ctx.shadowColor = "rgba(255,255,255,0.92)";ctx.shadowBlur = 7 * f;} ctx.fillStyle = `rgb(${cr},${cg},${cb})`; ctx.fillText(p.ch, p.x, p.y); ctx.restore(); } } // dissolve shards (Telegram-style break-up into tiny pieces) if (sparks.length) { for (let i = sparks.length - 1; i >= 0; i--) { const s = sparks[i]; s.x += s.vx;s.y += s.vy; s.vx *= 0.94;s.vy = s.vy * 0.94 + size * 0.0004; s.life -= 1; if (s.life <= 0) {sparks.splice(i, 1);continue;} const sa = Math.pow(s.life / s.max, 0.85); const d = s.sz * (0.5 + sa * 0.6); ctx.fillStyle = `rgba(10,10,10,${(sa * 0.92).toFixed(3)})`; ctx.fillRect(s.x, s.y, d, d); } } raf = requestAnimationFrame(render); }; raf = requestAnimationFrame(render); return () => {stopped = true;cancelAnimationFrame(raf);window.removeEventListener("pointermove", onMove);}; }, [size]); return ; } /* ============== HERO ORB — composition wrapper (backdrop + sphere) ============== */ function HeroOrb({ size = 520 }) { // wrapper matches the actual visual extent so the sphere never overflows the layout const visual = size * 1.2; return (
); } /* ============== CURSOR ============== */ function Cursor({ enabled }) { const dot = useRef(null),ring = useRef(null); const [mode, setMode] = useState("idle"); const [label, setLabel] = useState(""); useEffect(() => { if (!enabled) {document.body.classList.remove("has-cursor");return;} document.body.classList.add("has-cursor"); let mx = innerWidth / 2,my = innerHeight / 2,rx = mx,ry = my,dx = mx,dy = my,raf; const tick = () => { rx += (mx - rx) * 0.18;ry += (my - ry) * 0.18; dx += (mx - dx) * 0.45;dy += (my - dy) * 0.45; if (dot.current) dot.current.style.transform = `translate(${dx}px, ${dy}px) translate(-50%, -50%)`; if (ring.current) ring.current.style.transform = `translate(${rx}px, ${ry}px) translate(-50%, -50%)`; raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); const onMove = (e) => {mx = e.clientX;my = e.clientY;}; const onOver = (e) => { const t = e.target.closest?.("[data-cursor]"); if (!t) {setMode("idle");setLabel("");return;} const m = t.dataset.cursor;const l = t.dataset.cursorLabel || ""; setMode(l ? "text" : m || "big");setLabel(l); }; const onLeave = () => {setMode("idle");setLabel("");}; addEventListener("pointermove", onMove); document.addEventListener("pointerover", onOver); document.addEventListener("pointerout", onLeave); return () => { cancelAnimationFrame(raf); removeEventListener("pointermove", onMove); document.removeEventListener("pointerover", onOver); document.removeEventListener("pointerout", onLeave); document.body.classList.remove("has-cursor"); }; }, [enabled]); if (!enabled) return null; return ( <>
{label ? {label} : null}
); } /* ============== LOADER ============== */ function Loader({ active, onDone }) { const [pct, setPct] = useState(0); const [lifting, setLifting] = useState(false); useEffect(() => { if (!active) return; let n = 0; const id = setInterval(() => { n = Math.min(100, n + Math.random() * 16 + 6); setPct(Math.floor(n)); if (n >= 100) { clearInterval(id); setTimeout(() => setLifting(true), 320); setTimeout(() => onDone?.(), 1400); } }, 95); return () => clearInterval(id); }, [active, onDone]); if (!active) return null; return (
.v1
{pct.toString().padStart(3, "0")} / 100
); } /* ============== NAV + MENU OVERLAY ============== */ function Nav({ onMenu }) { const y = useScrollY(); return ( ); } function MenuOverlay({ open, onClose }) { const items = [ { label: "Применения", href: "#Применения" }, { label: "Домены", href: "#Домены" }, { label: "Кейсы", href: "pages/cases.html" }, { label: "Услуги", href: "pages/services.html" }, { label: "Команда", href: "pages/team.html" }, { label: "Медиа", href: "pages/media.html" }, { label: "О компании", href: "pages/about.html" }, { label: "Контакты", href: "#Контакты" }]; return (
.v1
Email
office@v1data.ru
Телефон
+7 (499) 117-00-87
Партнёрство
partners@v1data.ru
Офис
Москва · 2026
); } /* ============== HERO ============== */ function Hero() { const time = useClock(); return (
{/* sphere — sits just under the nav, fully visible */}
{/* satellite cards — stacked vertically on the right, overlaying the sphere */}
[3]
Восстановление
slip / collision / mis-pick
[1]
≥ 99,2%
AI-предразметка по DROID
[2]
≤ 50 мс
Sync video ↔ states
{/* bottom row: H1 left, announcement pill right — overlaps the sphere bottom */}
); } /* ============== SCENARIOS (alt dark/light cards with decor) — section 01 ============== */ function Scenarios() { const ref = useReveal(); const items = [ { kind: "dark", vis: "humanoid", name: "Гуманоиды", desc: "VLA-датасеты для антропоморфных роботов с двуручной манипуляцией." }, { kind: "light", vis: "mobile", name: "Мобильные манипуляторы", desc: "Сбор данных для роботов с подвижной базой и навигацией в пространстве." }, { kind: "light", vis: "stationary", name: "Стационарные манипуляторы", desc: "Single и dual-arm на фиксированной базе для precision-задач." }, { kind: "dark", vis: "umi", name: "Девайсы UMI / Gloves", desc: "Сбор демонстраций без робота — портативные манипуляторы и сенсорные перчатки." }]; return (
Строим data-фундамент для VLA под любую форму воплощения робота.

Работаем с командами, которые стоят на пороге запуска, масштабирования или переосмысления своего robotics-стека.

{items.map((it, i) => { const dark = it.kind === "dark"; return (
); } /* ============== SCENARIO VISUAL — animated SVG per применение ============== */ function ScenarioVisual({ kind, dark }) { const stroke = dark ? "rgba(255,255,255,0.85)" : "rgba(10,10,10,0.75)"; const fill = dark ? "rgba(255,255,255,0.08)" : "rgba(10,10,10,0.05)"; const accent = "#EA0A23"; const sw = 1.5; return ( ); } /* ============== DOMAIN VISUAL — animated SVG per kind ============== */ function DomainVisual({ kind, stroke, dark }) { const accent = "#EA0A23"; const sw = 1.4; return ( ); } /* ============== INDUSTRIES — large slider with header + arrows — section 02 ============== */ function Industries() { const ref = useReveal(); const scroller = useRef(null); const items = [ { name: "Кухня", desc: "Кухонный домен — эпизоды приготовления, сервировки и уборки. Деформируемые объекты и жидкости.", kind: "kitchen" }, { name: "Лаборатория", desc: "Лабораторные задачи — пипетирование, работа с пробирками и прозрачными объектами.", kind: "lab" }, { name: "Ритейл", desc: "Ритейл — сканирование, выкладка, работа с SKU. Кассовые и складские сценарии.", kind: "retail" }, { name: "Уборка", desc: "Уборка — протирка, сбор мусора, взаимодействие с поверхностями и разными материалами.", kind: "cleaning" }, { name: "Производство", desc: "Сборочные линии и станки — точные манипуляции, F/T-сенсорика, повторяемые операции.", kind: "factory" }, { name: "Офис", desc: "Офисные сценарии — работа с документами, перемещения, взаимодействия с оргтехникой.", kind: "office" }, { name: "Сложные сценарии", desc: "Ткани, прозрачные объекты, узкие допуски и бликующие материалы. Не менее 10% в каждой поставке.", kind: "hard" }]; const scrollBy = (dir) => scroller.current?.scrollBy({ left: dir * 420, behavior: "smooth" }); React.useEffect(() => { // ensure the slider starts at 0 — prevents scroll-snap from "eating" the left padding on mount const el = scroller.current; if (el) el.scrollLeft = 0; }, []); return (
{/* header: left meta, right lead text + arrows */}

Покрываем ключевые домены реального мира — от бытовых сценариев до промышленных сборочных линий.

Собираем мультимодальные данные в доменах, где роботы выходят из лабораторий в реальность.

{/* horizontal slider with large square cards */}
{items.map((it, i) => { const dark = i === 0; const accentText = dark ? "#fff" : "var(--ink)"; const stroke = dark ? "rgba(255,255,255,0.55)" : "rgba(10,10,10,0.7)"; return (
0{i + 1} / 0{items.length}
{/* domain-specific animated visual */}
{it.name}

{it.desc}

); })}
); } /* ============== CASES — asymmetric Bento, each card own color — section 03 ============== */ function Cases() { const ref = useReveal(); // each case in its own original color (not lifted from anyone) const palette = { mk: { bg: "#EA0A23", text: "#fff" }, // accent red fn: { bg: "#0A0A0A", text: "#fff" }, // black yu: { bg: "#1A1A1A", text: "#fff" }, // off-black hr: { bg: "#F2F1EC", text: "#0A0A0A" }, // light card sp: { bg: "#EFEDE6", text: "#0A0A0A" }, // light card 2 op: { bg: "#EA0A23", text: "#fff" } // red big }; return (

Что уже сделали для первых партнёров — от пилотного эпизода до production-датасетов.

); } function CaseCard({ color, tag, title, sub, kpi, big, href, chip, decoration, oneLine }) { const onDark = color.text === "#fff"; const Tag = href ? "a" : "article"; return (
{tag}
{big && decoration === "hand" ?
: null} {big && decoration !== "hand" ?
: null} {decoration === "pig" ?
{/* dotted grid background */}
{/* heatmap glow bottom-right */}
{/* skeletal trace SVG */} {/* pig silhouette side-view */} {/* snout + ear */} {/* legs */} {/* skeletal connecting lines */} {/* 15 keypoints — last one (right rear hoof) is the anomaly */} {/* anomaly: right rear hoof — stays red */}
: null} {decoration === "hand" ?
: null} {decoration === "gripper" ?
{/* wrist + mount */} {/* detection arc */} {/* re-grasp pulse */} {/* left jaw */} {/* right jaw */} {/* object */} {/* slip marker */} {/* recovered check */}
: null} {decoration === "pipeline" ?
{/* connectors */} {/* blocks */} {/* labels */} INGEST STORE LABEL EXPORT {/* scan bar */} {/* PASS / WARN indicators */}
2 / 4 PASS · 1 / 4 WARN
: null} {decoration === "amr" ?
{/* shelves (outline rectangles) */} {/* dashed zig-zag route */} {/* station ticks (green, appear as AMR passes) */} {/* the AMR dot travelling the route (CSS offset-path for hover control) */}
: null} {decoration === "amr" ? Смотреть кейс ↗ : null} {decoration === "race" ?
{/* MANUAL */} MANUAL {/* AI PRE-LABEL */} AI PRE-LABEL {/* ×8 result */} × 8
: null} {decoration === "race" ? Смотреть кейс ↗ : null}
{title}
{sub}
{kpi ?
{kpi.v}
{kpi.l}
: null} {chip ?
{chip}
: null}
); } /* ============== SERVICES (accordion: pill list left + colored panel right) — section 04 ============== */ function Services() { const services = [ { key: "plat", name: "Платформа разметки", desc: "Self-serve среда: загрузка эпизодов, AI pre-label, human review, экспорт в LeRobot / RT-X / OXE. От 100 000 ₽/мес." }, { key: "collect", name: "Сбор данных под ключ", desc: "Managed teleoperation + ego-recording + UMI + data gloves. Sync ≤ 50 мс, F/T sensors, tactile maps. От 7 000 ₽ за час." }, { key: "curated", name: "Готовые датасеты", desc: "Готовые пакеты: Kitchen-1000h, Lab-Manipulation, Recovery-Pack, Deformables-Set. От 500 000 ₽ за 100 часов." }, { key: "recovery", name: "Разметка recovery как услуга", desc: "Единственный SKU разметки ошибок: 5–15% recovery + 5% hard failures + 80–90% success. Add-on +30%." }, { key: "consult", name: "ИТ-консалтинг", desc: "Аудит pipeline сбора и разметки. Интервью с CTO, анализ stack, документ с рекомендациями. По запросу." }]; const [active, setActive] = useState("plat"); const cur = services.find((s) => s.key === active); const idx = services.findIndex((s) => s.key === active); // Auto-cycle through services at a readable pace. Resets whenever `active` // changes — so a manual click restarts the timer instead of fighting it. useEffect(() => { const t = setTimeout(() => { setActive(services[(idx + 1) % services.length].key); }, 4600); return () => clearTimeout(t); }, [active, idx]); return (

Работаем как технологический партнёр под VLA-данные — от первого эпизода до production.

{services.map((s) => )}
{/* corner decor: 4-dot */}
{/* top right diagonal dots */}
{[1, 2, 3, 4].map((i) => )}
04 · услуга · {String(idx + 1).padStart(2, "0")} / {String(services.length).padStart(2, "0")}
{cur.name}

{cur.desc}

{/* ===== Featured product плашка: V1 Data Capture ===== */}
); } /* ============== TEAM — auto-scrolling marquee of REAL people (synced with /pages/team) ============== */ function Team() { const ref = useReveal(); // Same roster as on the team page. Initials are derived from "name". const employees = [ { name: "Иван Голяков", role: "Генеральный директор · основатель", dept: "Лидерство", quote: "Данные — это язык, на котором роботы учатся понимать наш мир. Наша работа — написать его начисто." }, { name: "Максим Орлов", role: "Технический директор", dept: "Лидерство", quote: "Модель не бывает умнее данных, на которых выросла. Качество разметки — это потолок интеллекта машины." }, { name: "Анна Лебедева", role: "Руководитель операций по данным", dept: "Лидерство", quote: "За каждым уверенным движением робота стоит тысяча аккуратно размеченных кадров. Порядок здесь рождает надёжность." }, { name: "Дмитрий Романов", role: "Руководитель информационной безопасности", dept: "Лидерство", quote: "Доверие к данным начинается с их защиты. Чистый и безопасный датасет — фундамент, на котором держится всё остальное." }, { name: "Кирилл Громов", role: "Руководитель VLA-исследований", dept: "Робототехника · ML", quote: "Робот учится не на идеале, а на честно размеченной реальности — со всеми её ошибками и восстановлениями." }, { name: "Егор Васильев", role: "Старший инженер-робототехник", dept: "Робототехника · ML", quote: "Самый сильный момент — когда железо впервые повторяет жест человека. Этот мост строится из данных." }, { name: "Сергей Кузнецов", role: "Инженер симуляции", dept: "Робототехника · ML", quote: "В симуляции я создаю тысячи миров. Хорошая разметка — то, что делает их правдой для робота." }, { name: "Полина Соловьёва", role: "ML-инженер · AI-предразметка", dept: "Робототехника · ML", quote: "Предразметка — это диалог человека и модели. Машина предлагает, человек выверяет — так рождается точность." }, { name: "Никита Зайцев", role: "Руководитель инженерии платформы", dept: "Платформа", quote: "Хорошая платформа незаметна: она просто даёт данным течь чисто и быстро. В этой тишине — вся инженерия." }, { name: "Татьяна Морозова", role: "Старший дата-инженер", dept: "Платформа", quote: "Данные любят дисциплину. Когда поток выстроен верно, из хаоса сырых логов рождается знание." }, { name: "Роман Белов", role: "Frontend-инженер", dept: "Платформа", quote: "Разметчик проводит с интерфейсом часы. Сделать эти часы лёгкими — тихая, но важная работа." }, { name: "Юрий Воронцов", role: "DevOps-инженер · SRE", dept: "Платформа", quote: "Надёжность — это когда конвейер данных работает, а о нём не вспоминают. Стабильность дороже скорости." }, { name: "Виктор Тихонов", role: "Руководитель teleop-студии", dept: "Teleop-фабрика", quote: "Через телеоперацию мы передаём роботу человеческую интуицию. Каждый эпизод — урок, записанный руками." }, { name: "Ольга Новикова", role: "Старший руководитель разметки", dept: "Teleop-фабрика", quote: "Разметка — это ремесло внимания. Один честно отмеченный кадр стоит сотни сделанных наспех." }, { name: "Александр Беляев", role: "Специалист по recovery-сценариям", dept: "Teleop-фабрика", quote: "Робот взрослеет не тогда, когда не ошибается, а когда умеет исправиться. Этому мы его и учим." }, { name: "Мария Дмитриева", role: "Аудитор качества", dept: "Teleop-фабрика", quote: "Качество не проверяют в конце — его выстраивают на каждом шаге. Я страж этой правды в данных." }, { name: "Лилия Котова", role: "Руководитель Customer Success", dept: "Работа с клиентами", quote: "Лучшие данные — те, что решают реальную задачу клиента. Я слежу, чтобы наша точность становилась их результатом." }, { name: "Артём Карпов", role: "Sales-инженер", dept: "Работа с клиентами", quote: "Я объясняю инженерам ценность чистых данных их же языком. Честная цифра убеждает лучше любых слов." }, { name: "Ксения Жукова", role: "Менеджер партнёрств", dept: "Работа с клиентами", quote: "Робототехника движется вперёд сообща. Хорошее партнёрство, как и хорошие данные, строится на доверии." }, { name: "Глеб Михайлов", role: "Старший продуктовый дизайнер", dept: "Работа с клиентами", quote: "Дизайн для разметки — забота о тех, кто часами всматривается в детали. Ясность снижает усталость и ошибки." }]; // duplicate for seamless marquee loop const loop = [...employees, ...employees]; const initial = (n) => (n.trim().charAt(0) || "·").toUpperCase(); return (

Робототехники, ML-исследователи, операторы teleop-станций и инженеры платформы. Вся команда →

); } /* ============== MEDIA — horizontal slider (placeholders) — section 06 ============== */ function Media() { const scroller = useRef(null); const articles = [ { tag: "ГАЙД", source: "PDF", title: "Как собрать первый VLA-датасет за 30 дней", date: "Май 2026", abstract: "Пошаговая инструкция для команд, которые раньше не собирали robotics-данных — от episode-структуры до экспорта.", href: "pages/media/vla-dataset-30-days.html" }, { tag: "БЕНЧМАРК", source: "блог", title: "DROID vs наш AI Pre-Label: IoU и скорость", date: "Апрель 2026", abstract: "Открытый отчёт о том, как предразметка влияет на скорость и точность human review на DROID benchmark.", href: "pages/media/droid-vs-ai-pre-label.html" }, { tag: "КЕЙС", source: "видео", title: "Pi0.5 в кухонном домене: 800 эпизодов за неделю", date: "Март 2026", abstract: "12-минутный разбор полного цикла — от постановки до eval и выводов по Recovery-квотам.", href: "pages/media/pi05-kitchen-domain.html" }, { tag: "СТАТЬЯ", source: "внешка", title: "Разметка данных под робототехнику: почему generic-платформы не работают", date: "Февраль 2026", abstract: "О том, почему VLA требуют особого tooling для proprioception, F/T-сенсорики и episode-timecodes.", href: "pages/media/robot-native-data-labeling.html" }, { tag: "ГАЙД", source: "блог", title: "Recovery-эпизоды: что и зачем размечать", date: "Январь 2026", abstract: "Таксономия failure-классов, протокол error → recovery → resume, производственные квоты.", href: "pages/media/recovery-episodes.html" }, { tag: "ПОДКАСТ", source: "эфир", title: "Внутри .v1: разговор о сборе данных для VLA", date: "Декабрь 2025", abstract: "47-минутный эфир с CTO о платформе, требованиях клиентов и sync ≤ 50 мс.", href: "pages/media/v1-podcast-vla-data.html" }, { tag: "WHITEPAPER", source: "PDF", title: "Sync ≤ 50 мс: как мы синхронизируем мультисенсорику", date: "Ноябрь 2025", abstract: "Hardware-и software-методы синхронизации видео, проприоцепции и F/T в едином временном пространстве.", href: "pages/media/sync-50ms-multimodal.html" }]; const scrollBy = (dir) => scroller.current?.scrollBy({ left: dir * innerWidth * 0.7, behavior: "smooth" }); return (

Медиацентр

); } /* ============== ABOUT — two-column with dark particle side — section 07 ============== */ function About() { return (

.v1

Платформа разметки мультимодальных данных для VLA-моделей и гуманоидов. Помогаем roboticists получать качественные данные — от первого эпизода до миллиона часов.

.v Группа V1
О компании
{Array.from({ length: 220 }, (_, i) => { const a = Math.random() * Math.PI * 2; const r = 80 + Math.random() * 220; const cx = 300 + Math.cos(a) * r; const cy = 270 + Math.sin(a) * r * 0.6; const sz = Math.random() * 2 + 0.5; return 0.85 ? "var(--accent)" : "#fff"} opacity={0.3 + Math.random() * 0.5} />; })}
.v1 / 2026
Teleop фабрики
Москва
); } /* ============== TELEGRAM DELIVERY (via backend on Beget VPS) ============== */ const API_BASE = "https://v1data.ru"; // прод-домен; backend подключается через /api/* async function sendToTelegram(payload) { try { const r = await fetch(`${API_BASE}/api/submit.php`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); const json = await r.json().catch(() => ({})); return { ok: r.ok && json.ok, error: json.error }; } catch (e) { return { ok: false, error: "Сеть недоступна" }; } } /* ============== CONTACT FORM (with success state) ============== */ function ContactForm() { const [state, setState] = useState("idle"); // idle | sending | sent | error const [errMsg, setErrMsg] = useState(""); const submit = async (e) => { e.preventDefault(); setState("sending"); setErrMsg(""); const f = e.target; const data = { name: f.elements.namedItem("name")?.value, company: f.elements.namedItem("company")?.value, inn: f.elements.namedItem("inn")?.value, phone: f.elements.namedItem("phone")?.value, email: f.elements.namedItem("email")?.value, robotType: f.elements.namedItem("robotType")?.value, dataVolume: f.elements.namedItem("dataVolume")?.value, comment: f.elements.namedItem("comment")?.value, source: "Главная · /#contact", _hp: f.elements.namedItem("_hp")?.value // honeypot }; const result = await sendToTelegram(data); if (result.ok) { setState("sent"); } else { setState("error"); setErrMsg(result.error || "Не удалось отправить заявку. Попробуйте позже."); } }; if (state === "sent") { return (

Заявка отправлена

Спасибо! Мы свяжемся в течение 24 часов в рабочие дни. Ответ придёт с office@v1data.ru.

); } return (
{/* honeypot — скрыт от пользователя, заполняется ботами */}