Files
roi24/hero-v3.html
neo 9ba79075a9 hero-v3: tunnel parallax — speed lines + flying KPI cards at 3 depths
Tunnel engine:
- 30 speed lines (near/mid/far) rushing left→right
- 6 KPI cards at 3 depths: nearest blurry+large, far sharp+small
- Cruise speed 0.8, boost 2.5× on CTA hover
- 40% cards start in viewport for instant visual
- Motion blur: cards blur proportional to speed (0→4px)
- Glow expands & brightens with speed

Drive-the-car metaphor: hover = gas pedal, cards & lines accelerate
Like driving through a tunnel with business data flying by
2026-06-21 21:20:01 +05:00

463 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ROI24 — Tunnel Hero</title>
<link rel="preconnect" href="https://fonts.cdnfonts.com">
<link href="https://fonts.cdnfonts.com/css/inter" rel="stylesheet">
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet">
<style>
/* ============================================================
DESIGN TOKENS
============================================================ */
:root {
--bg: #0a0a0a;
--text: #f5f5f5;
--text-sec: #a1a1aa;
--text-dim: #52525b;
--accent: #22c55e;
--accent-dim: rgba(34,197,94,0.12);
--blue: #3b82f6;
--blue-dim: rgba(59,130,246,0.12);
--amber: #f59e0b;
--red: #ef4444;
--font: 'Inter', -apple-system, sans-serif;
--mono: 'JetBrains Mono', 'SF Mono', monospace;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg); color: var(--text); font-family: var(--font);
overflow-x: hidden; min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* ============================================================
TUNNEL CONTAINER — perspective origin at center
============================================================ */
.tunnel {
position: relative; width: 100%; height: 100vh;
overflow: hidden;
perspective: 800px;
perspective-origin: 50% 50%;
}
/* ============================================================
CENTER GLOW — the light at the end of the tunnel
============================================================ */
.tunnel-glow {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 600px; height: 600px; border-radius: 50%;
background: radial-gradient(circle,
rgba(34,197,94,0.12) 0%,
rgba(34,197,94,0.04) 30%,
rgba(34,197,94,0.01) 55%,
transparent 70%
);
pointer-events: none; z-index: 1;
animation: glowBreath 3s ease-in-out infinite;
}
@keyframes glowBreath {
0%, 100% { opacity: 0.6; transform: translate(-50%, -50%) scale(1); }
50% { opacity: 1; transform: translate(-50%, -50%) scale(1.08); }
}
/* ============================================================
SPEED LINES — horizontal streaks rushing past
Three depth layers: near (fast), mid, far (slow)
============================================================ */
.speed-lines { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.speed-line {
position: absolute; height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(255,255,255,0.06) 30%,
rgba(34,197,94,0.15) 50%,
rgba(255,255,255,0.04) 70%,
transparent 100%
);
filter: blur(0.5px);
}
/* Near layer — thick, fast, blurred */
.speed-line.near {
height: 2px; opacity: 0.7;
filter: blur(1.5px);
background: linear-gradient(90deg,
transparent 0%,
rgba(255,255,255,0.1) 35%,
rgba(34,197,94,0.25) 50%,
rgba(255,255,255,0.06) 65%,
transparent 100%
);
}
.speed-line.mid { height: 1.5px; opacity: 0.4; filter: blur(0.8px); }
.speed-line.far { height: 1px; opacity: 0.2; filter: blur(0.3px); }
/* ============================================================
KPI CARDS — flying through the tunnel at different depths
============================================================ */
.kpi-card {
position: absolute;
background: rgba(22,22,24,0.8);
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 12px;
padding: 14px 16px;
width: 200px;
pointer-events: none;
transform-style: preserve-3d;
transition: filter 0.3s ease;
z-index: 10;
}
/* Depth layers via translateZ */
.kpi-card.depth-1 { /* nearest — large, fast, blurred */
box-shadow: 0 0 40px rgba(34,197,94,0.08);
border-color: rgba(255,255,255,0.1);
}
.kpi-card.depth-2 { /* mid */
box-shadow: 0 0 20px rgba(0,0,0,0.4);
}
.kpi-card.depth-3 { /* far — small, slow, sharp */
box-shadow: none;
border-color: rgba(255,255,255,0.04);
}
.kpi-card .label {
font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em;
color: var(--text-dim); font-weight: 600; margin-bottom: 4px;
}
.kpi-card .value {
font-family: var(--mono); font-weight: 700; line-height: 1.15;
margin-bottom: 3px;
}
.kpi-card .value.green { color: var(--accent); }
.kpi-card .value.blue { color: var(--blue); }
.kpi-card .value.amber { color: var(--amber); }
.kpi-card .trend {
display: inline-flex; align-items: center; gap: 3px;
padding: 2px 5px; border-radius: 4px;
font-family: var(--mono); font-weight: 600; font-size: 9px;
}
.kpi-card .trend.up { color: var(--accent); background: var(--accent-dim); }
.kpi-card .trend.down { color: var(--red); background: rgba(239,68,68,0.12); }
/* Motion blur on cards — depends on speed */
.kpi-card .trail {
position: absolute; inset: 0; border-radius: 12px;
background: inherit; opacity: 0;
pointer-events: none;
z-index: -1;
}
/* ============================================================
CENTER CONTENT — the destination
============================================================ */
.hero-center {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
text-align: center; z-index: 30; max-width: 600px;
width: 100%; padding: 0 24px;
}
.hero-badge {
display: inline-flex; align-items: center; gap: 8px;
padding: 5px 14px; border: 1px solid rgba(255,255,255,0.1);
border-radius: 100px; margin-bottom: 20px;
font-size: 11px; color: var(--text-sec); font-family: var(--mono);
background: rgba(255,255,255,0.02);
}
.badge-dot {
width: 5px; height: 5px; background: var(--accent); border-radius: 50%;
animation: dotPulse 1.5s ease-in-out infinite;
}
@keyframes dotPulse {
0%, 100% { box-shadow: 0 0 3px var(--accent); }
50% { box-shadow: 0 0 10px var(--accent); }
}
.hero-center h1 {
font-size: clamp(1.8rem, 5vw, 4rem);
font-weight: 800; letter-spacing: -0.04em;
line-height: 1.08; margin-bottom: 16px;
}
.hero-center h1 .accent { color: var(--accent); }
.hero-center .sub {
font-size: 1rem; color: rgba(255,255,255,0.4);
line-height: 1.6; margin-bottom: 32px; max-width: 460px;
margin-left: auto; margin-right: auto;
}
.btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
padding: 14px 28px; border-radius: 100px; font-weight: 600;
font-size: 0.9rem; cursor: pointer; border: none;
transition: all 0.25s ease; text-decoration: none;
}
.btn-accent {
background: var(--accent); color: #000;
}
.btn-accent:hover {
background: #16a34a; transform: translateY(-1px);
box-shadow: 0 12px 40px rgba(34,197,94,0.25);
}
.btn-outline {
background: transparent; border: 1px solid rgba(255,255,255,0.2);
color: var(--text);
}
.btn-outline:hover {
border-color: rgba(255,255,255,0.4);
background: rgba(255,255,255,0.03);
}
/* ============================================================
RESPONSIVE
============================================================ */
@media (max-width: 768px) {
.kpi-card { width: 150px; padding: 10px 12px; }
.kpi-card .value { font-size: 16px; }
.speed-line.near { opacity: 0.4; }
}
@media (max-width: 480px) {
.kpi-card { width: 120px; padding: 8px 10px; }
.kpi-card .value { font-size: 14px; }
.kpi-card .label { font-size: 8px; }
.speed-line.near, .speed-line.mid { opacity: 0.25; }
}
</style>
</head>
<body>
<div class="tunnel" id="tunnel">
<!-- Center glow -->
<div class="tunnel-glow" id="tunnelGlow"></div>
<!-- Speed lines -->
<div class="speed-lines" id="speedLines"></div>
<!-- KPI cards — injected by JS -->
<!-- Center content -->
<div class="hero-center">
<div class="hero-badge">
<span class="badge-dot"></span>
АНАЛИТИКА ДЛЯ СОБСТВЕННИКОВ
</div>
<h1>Живые цифры<br>вашего <span class="accent">бизнеса</span></h1>
<p class="sub">
10 ключевых метрик из 1С, CRM и банков —<br>
автоматически, каждое утро в телефоне
</p>
<div class="btns">
<a href="#cta" class="btn btn-accent"
onmouseenter="setSpeed(1)"
onmouseleave="setSpeed(0)">
Получить демо за 3 дня
</a>
<a href="#demo" class="btn btn-outline"
onmouseenter="setSpeed(1)"
onmouseleave="setSpeed(0)">
Посмотреть пример
</a>
</div>
</div>
</div>
<script>
/* ============================================================
TUNNEL ENGINE — cards & speed lines fly past at variable speed
============================================================ */
const tunnel = document.getElementById('tunnel');
const speedLinesEl = document.getElementById('speedLines');
const glow = document.getElementById('tunnelGlow');
// ── SPEED STATE ──
let targetSpeed = 0; // 0 = cruise, 1 = full throttle
let currentSpeed = 0; // lerped
const CRUISE = 0.8; // baseline perpetual motion
const BOOST = 2.5; // max throttle multiplier
// ── SPEED LINES ──
const LINES_COUNT = 30;
const lines = [];
function createSpeedLines() {
speedLinesEl.innerHTML = '';
const vw = window.innerWidth;
for (let i = 0; i < LINES_COUNT; i++) {
const div = document.createElement('div');
const depthRoll = Math.random();
let cls, w;
if (depthRoll < 0.18) {
cls = 'near'; w = 80 + Math.random() * 120;
} else if (depthRoll < 0.45) {
cls = 'mid'; w = 40 + Math.random() * 80;
} else {
cls = 'far'; w = 20 + Math.random() * 40;
}
div.className = `speed-line ${cls}`;
div.style.top = Math.random() * 100 + '%';
div.style.width = w + '%';
speedLinesEl.appendChild(div);
// Start randomly across the viewport + off-screen right
const startX = -w * vw / 100 + Math.random() * (vw + w * vw / 100);
lines.push({ el: div, cls, width: w, offset: startX, speedMul: cls === 'near' ? 3.5 : cls === 'mid' ? 2 : 1 });
}
}
// ── KPI CARDS ──
const CARD_DATA = [
{ label: 'Выручка за 30 дней', value: '2 051 857 ₽', trend: '↑ +15%', dir: 'up', color: 'green', depth: 1, side: 'left' },
{ label: 'CPA средний', value: '890 ₽', trend: '↓ −5%', dir: 'down', color: 'blue', depth: 2, side: 'left' },
{ label: 'ROI рекламы', value: '91.8%', trend: '↑ +5%', dir: 'up', color: 'amber', depth: 2, side: 'right' },
{ label: 'Прибыль', value: '981 857 ₽', trend: '↑ +12%', dir: 'up', color: 'green', depth: 1, side: 'right' },
{ label: 'Продажи', value: '1 247 шт', trend: '↑ +12%', dir: 'up', color: 'blue', depth: 3, side: 'left' },
{ label: 'Конверсия', value: '2.8%', trend: '↑ +0.3%',dir: 'up', color: 'amber', depth: 3, side: 'right' },
];
const cards = [];
function createCards() {
CARD_DATA.forEach((d, i) => {
const div = document.createElement('div');
div.className = `kpi-card depth-${d.depth}`;
div.innerHTML = `
<div class="label">${d.label}</div>
<div class="value ${d.color}">${d.value}</div>
<span class="trend ${d.dir}">${d.trend}</span>
`;
tunnel.appendChild(div);
const rect = div.getBoundingClientRect();
const vw = window.innerWidth;
// Starting position: spread across viewport, some visible immediately
let startX;
if (Math.random() < 0.4) {
// Start inside viewport for immediate visibility
const buffer = rect.width + 40;
startX = d.side === 'left'
? buffer + Math.random() * (vw * 0.3)
: vw - buffer - Math.random() * (vw * 0.3);
} else {
startX = d.side === 'left'
? -rect.width - 100 - Math.random() * 300
: window.innerWidth + 100 + Math.random() * 200;
}
const startY = 15 + Math.random() * 70; // % of viewport height
div.style.top = startY + '%';
div.style.left = startX + 'px';
div.style.transform = `scale(${1 - d.depth * 0.12})`;
cards.push({
el: div,
depth: d.depth,
side: d.side,
x: startX,
y: startY,
scale: 1 - d.depth * 0.12,
speed: 0.5 + (4 - d.depth) * 0.5, // depth1=2.0, depth2=1.5, depth3=1.0
});
});
}
// ── ANIMATION LOOP ──
function loop() {
const dt = 1 / 60; // approximate frame delta
const lerp = 0.035;
// Smooth speed transition
const effectiveTarget = CRUISE + targetSpeed * BOOST;
currentSpeed += (effectiveTarget - currentSpeed) * lerp;
// ── Move speed lines ──
const vw = window.innerWidth;
lines.forEach(line => {
line.offset += currentSpeed * line.speedMul * 2.5;
const lineW = line.width * vw / 100;
// Reset when fully off-screen right
if (line.offset > vw + 100) {
line.offset = -lineW - Math.random() * 300;
line.el.style.top = Math.random() * 100 + '%';
}
line.el.style.transform = `translateX(${line.offset}px)`;
});
// ── Move KPI cards ──
cards.forEach(card => {
const cardSpeed = card.speed * currentSpeed * 1.2;
const dir = card.side === 'left' ? 1 : -1;
card.x += cardSpeed * dir;
const cardW = card.el.offsetWidth;
const cardH = card.el.offsetHeight;
// Reset when off-screen
const offRight = card.side === 'left' && card.x > vw + 100;
const offLeft = card.side === 'right' && card.x < -cardW - 100;
if (offRight || offLeft) {
card.x = card.side === 'left'
? -cardW - Math.random() * 400
: vw + Math.random() * 200;
card.y = 10 + Math.random() * 75;
card.el.style.top = card.y + '%';
}
// Motion blur via filter
const blur = Math.min(currentSpeed * 2.5, 4);
card.el.style.filter = `blur(${blur}px)`;
card.el.style.left = card.x + 'px';
card.el.style.transform = `scale(${card.scale})`;
});
// ── Glow reacts to speed ──
const glowScale = 1 + currentSpeed * 0.15;
const glowOpacity = 0.5 + currentSpeed * 0.6;
glow.style.transform = `translate(-50%, -50%) scale(${glowScale})`;
glow.style.opacity = Math.min(glowOpacity, 1.3);
requestAnimationFrame(loop);
}
// ── SPEED CONTROL ──
function setSpeed(active) {
targetSpeed = active ? 1 : 0;
}
// ── INIT ──
createSpeedLines();
createCards();
requestAnimationFrame(loop);
// Handle resize
window.addEventListener('resize', () => {
// Reset card positions to spread across viewport
const vw = window.innerWidth;
cards.forEach(card => {
if (card.x > vw + 200 || card.x < -card.el.offsetWidth - 200) {
card.x = card.side === 'left'
? -card.el.offsetWidth - Math.random() * 300
: vw + Math.random() * 200;
}
});
});
</script>
</body>
</html>