Files
roi24/hero-v4.html
T
neo 2605318513 hero-v4: perspective tunnel — cards on angled walls, radial speed lines
Forward motion (not sideways):
- Tunnel walls: rotateY(12°) left, rotateY(-12°) right
- 10 KPI cards slide down walls with perspective scale (smaller = farther)
- Cards reset when off-screen, pick new random data
- 50 radial lines emanating from center outward
- 80 starfield dots scattering from center
- Center: clean — just headline + CTA, no data
- Goo glow expands & brightens with speed
- Hover = accelerate forward (CRUISE 0.5 → BOOST 2.5)

Metaphor: driving into a data tunnel, cards on walls rush past
2026-06-21 21:24:52 +05:00

504 lines
17 KiB
HTML
Raw 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 — Perspective 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: hidden; height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* ============================================================
PERSPECTIVE STAGE — the tunnel
============================================================ */
.stage {
position: relative; width: 100%; height: 100vh;
overflow: hidden;
perspective: 1000px;
perspective-origin: 50% 50%;
}
/* ============================================================
VISUAL EFFECTS LAYER
============================================================ */
.fx-layer {
position: absolute; inset: 0; pointer-events: none; z-index: 1;
}
/* Noise texture */
.fx-layer::before {
content: ''; position: absolute; inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.02'/%3E%3C/svg%3E");
opacity: 0.5;
}
/* Goo — ambient light ahead */
.goo {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 500px; height: 500px; border-radius: 50%;
background: radial-gradient(circle,
rgba(34,197,94,0.10) 0%,
rgba(34,197,94,0.03) 35%,
transparent 65%
);
transition: transform 0.5s ease, opacity 0.5s ease;
}
/* ============================================================
TUNNEL WALLS — cards on left & right with 3D perspective
============================================================ */
.wall {
position: absolute; top: 50%; z-index: 20;
transform-style: preserve-3d;
pointer-events: none;
}
.wall-left {
left: 4%;
transform: translateY(-50%) rotateY(12deg);
transform-origin: right center;
}
.wall-right {
right: 4%;
transform: translateY(-50%) rotateY(-12deg);
transform-origin: left center;
}
/* Each card is a "panel" on the tunnel wall */
.tunnel-card {
background: rgba(22,22,24,0.88);
backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,0.07);
border-radius: 10px;
padding: 12px 14px;
width: 170px;
margin-bottom: 16px;
transition: opacity 0.4s ease, filter 0.4s ease;
}
.tunnel-card .tl {
font-size: 9px; text-transform: uppercase; letter-spacing: 0.07em;
color: var(--text-dim); font-weight: 600; margin-bottom: 3px;
}
.tunnel-card .tv {
font-family: var(--mono); font-size: 17px; font-weight: 700;
line-height: 1.15; margin-bottom: 2px;
}
.tunnel-card .tv.g { color: var(--accent); }
.tunnel-card .tv.b { color: var(--blue); }
.tunnel-card .tv.a { color: var(--amber); }
.tunnel-card .td {
display: inline-flex; align-items: center; gap: 2px;
padding: 2px 5px; border-radius: 3px;
font-family: var(--mono); font-weight: 600; font-size: 9px;
}
.tunnel-card .td.up { color: var(--accent); background: var(--accent-dim); }
.tunnel-card .td.down { color: var(--red); background: rgba(239,68,68,0.12); }
/* ── RADIAL LINES ── lines from center outward */
.radial-line {
position: absolute; top: 50%; left: 50%;
height: 1px;
background: linear-gradient(90deg,
rgba(255,255,255,0.08) 0%,
rgba(34,197,94,0.2) 30%,
rgba(34,197,94,0.05) 60%,
transparent 100%
);
transform-origin: left center;
}
.radial-line.fast {
background: linear-gradient(90deg,
rgba(255,255,255,0.15) 0%,
rgba(34,197,94,0.35) 25%,
rgba(34,197,94,0.08) 55%,
transparent 100%
);
height: 1.5px;
}
/* ── DOTS — starfield particles ── */
.dot {
position: absolute; border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 4px var(--accent);
}
/* ============================================================
CENTER — clean, no data, just headline
============================================================ */
.center {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
text-align: center; z-index: 30; max-width: 560px;
width: 100%; padding: 0 24px;
}
.badge {
display: inline-flex; align-items: center; gap: 8px;
padding: 4px 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: dPulse 1.5s ease-in-out infinite;
}
@keyframes dPulse {
0%,100% { box-shadow: 0 0 3px var(--accent); }
50% { box-shadow: 0 0 10px var(--accent); }
}
.center h1 {
font-size: clamp(1.8rem, 5vw, 4rem);
font-weight: 800; letter-spacing: -0.04em;
line-height: 1.08; margin-bottom: 16px;
}
.center h1 .accent { color: var(--accent); }
.center .sub {
font-size: 1rem; color: rgba(255,255,255,0.4);
line-height: 1.6; margin-bottom: 32px;
}
.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) {
.wall-left { left: 1%; }
.wall-right { right: 1%; }
.tunnel-card { width: 140px; padding: 10px 12px; }
.tunnel-card .tv { font-size: 15px; }
}
@media (max-width: 480px) {
.tunnel-card { width: 110px; padding: 8px 10px; }
.tunnel-card .tv { font-size: 13px; }
.tunnel-card .tl { font-size: 8px; }
}
</style>
</head>
<body>
<div class="stage" id="stage">
<!-- FX LAYER -->
<div class="fx-layer" id="fxLayer">
<div class="goo" id="goo"></div>
</div>
<!-- TUNNEL WALLS -->
<div class="wall wall-left" id="wallLeft"></div>
<div class="wall wall-right" id="wallRight"></div>
<!-- CENTER -->
<div class="center">
<div class="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>
/* ============================================================
PERSPECTIVE TUNNEL ENGINE
Cards on walls slide as we "drive forward"
Radial lines from center outward
Dots scatter like starfield
============================================================ */
const stage = document.getElementById('stage');
const wallLeft = document.getElementById('wallLeft');
const wallRight = document.getElementById('wallRight');
const fxLayer = document.getElementById('fxLayer');
const goo = document.getElementById('goo');
// ── SPEED STATE ──
let targetSpeed = 0;
let currentSpeed = 0; // lerped: 0=cruise, 1=boost
const CRUISE = 0.5; // always some motion
const BOOST = 2.5;
// ── CARD POOL ──
const CARD_DATA = [
{ label: 'Выручка за 30 дн', value: '2 051 857 ₽', trend: '+15%', clr: 'g', dir: 'up', wall: 'left' },
{ label: 'CPA средний', value: '890 ₽', trend: '−5%', clr: 'b', dir: 'down', wall: 'left' },
{ label: 'Продажи', value: '1 247 шт', trend: '+12%', clr: 'b', dir: 'up', wall: 'left' },
{ label: 'ROI рекламы', value: '91.8%', trend: '+5%', clr: 'a', dir: 'up', wall: 'right' },
{ label: 'Прибыль', value: '981 857 ₽', trend: '+12%', clr: 'g', dir: 'up', wall: 'right' },
{ label: 'Конверсия', value: '2.8%', trend: '+0.3%',clr: 'a', dir: 'up', wall: 'right' },
];
// Track each card instance on the wall
let wallCards = []; // { el, wall, offset, speedM, opacity }
function spawnCard(data, offsetY) {
const div = document.createElement('div');
div.className = 'tunnel-card';
div.innerHTML = `
<div class="tl">${data.label}</div>
<div class="tv ${data.clr}">${data.value}</div>
<span class="td ${data.dir}">${data.dir === 'up' ? '↑' : '↓'} ${data.trend}</span>
`;
const wall = data.wall === 'left' ? wallLeft : wallRight;
wall.appendChild(div);
return { el: div, wall: data.wall, offset: offsetY, speedM: 0.8 + Math.random() * 1.2 };
}
// Initialize walls with staggered cards
function initWalls() {
wallLeft.innerHTML = '';
wallRight.innerHTML = '';
wallCards = [];
const leftData = CARD_DATA.filter(d => d.wall === 'left');
const rightData = CARD_DATA.filter(d => d.wall === 'right');
// Stagger vertically: some visible, some ready to slide in
leftData.forEach((d, i) => {
const y = -60 + i * 80 + Math.random() * 20;
wallCards.push(spawnCard(d, y));
});
rightData.forEach((d, i) => {
const y = -40 + i * 80 + Math.random() * 20;
wallCards.push(spawnCard(d, y));
});
// Add extra "flying" cards for depth
for (let i = 0; i < 4; i++) {
const side = Math.random() < 0.5 ? 'left' : 'right';
const data = CARD_DATA[Math.floor(Math.random() * CARD_DATA.length)];
const y = -150 - Math.random() * 100;
wallCards.push(spawnCard({ ...data, wall: side }, y));
}
}
// ── RADIAL LINES ──
let radialLines = []; // { el, angle, length, speedM, life }
function spawnRadialLine() {
const div = document.createElement('div');
const isFast = Math.random() < 0.2;
div.className = isFast ? 'radial-line fast' : 'radial-line';
const angle = Math.random() * 360;
const length = 30 + Math.random() * 120;
div.style.width = length + 'px';
div.style.transform = `rotate(${angle}deg) translateY(-50%)`;
div.style.opacity = '0';
fxLayer.appendChild(div);
return { el: div, angle, length, speedM: 0.3 + Math.random() * 1.0, life: 0 };
}
// Initialize radial lines
for (let i = 0; i < 50; i++) {
radialLines.push(spawnRadialLine());
}
// ── DOTS (starfield) ──
let dots = []; // { el, x, y, vx, vy, speedM, life }
function spawnDot() {
const div = document.createElement('div');
div.className = 'dot';
const size = 2 + Math.random() * 2;
div.style.width = size + 'px';
div.style.height = size + 'px';
// Start at center
const cx = window.innerWidth / 2;
const cy = window.innerHeight / 2;
div.style.left = cx + 'px';
div.style.top = cy + 'px';
div.style.opacity = '0';
fxLayer.appendChild(div);
const angle = Math.random() * Math.PI * 2;
return {
el: div, x: cx, y: cy,
vx: Math.cos(angle), vy: Math.sin(angle),
speedM: 0.5 + Math.random() * 2.0,
life: 0, maxLife: 60 + Math.random() * 120, // frames
};
}
for (let i = 0; i < 80; i++) {
dots.push(spawnDot());
}
// ── LOOP ──
function loop() {
const lerp = 0.04;
currentSpeed += (CRUISE + targetSpeed * BOOST - currentSpeed) * lerp;
const spd = currentSpeed; // shorthand
const vw = window.innerWidth;
const vh = window.innerHeight;
const cx = vw / 2;
const cy = vh / 2;
// ── RADIAL LINES ──
radialLines.forEach(line => {
line.life += spd * line.speedM * 1.5;
const dist = line.life;
const alpha = dist < 10 ? dist / 10 : Math.max(0, 1 - (dist - 10) / 80);
line.el.style.opacity = alpha;
line.el.style.transform = `rotate(${line.angle}deg) translateX(${dist}px)`;
// Reset
if (dist > 90) {
line.life = 0;
line.angle = Math.random() * 360;
line.el.style.transform = `rotate(${line.angle}deg) translateX(0px)`;
}
});
// ── DOTS (starfield) ──
dots.forEach(dot => {
dot.life += spd * dot.speedM;
dot.x += dot.vx * spd * dot.speedM * 2;
dot.y += dot.vy * spd * dot.speedM * 2;
dot.el.style.left = dot.x + 'px';
dot.el.style.top = dot.y + 'px';
const dx = dot.x - cx;
const dy = dot.y - cy;
const dist = Math.sqrt(dx*dx + dy*dy);
// Fade in near center, bright at mid distance, fade out at edges
let alpha;
if (dist < 20) alpha = dist / 20 * 0.6;
else if (dist < vw * 0.8) alpha = 0.3 + 0.3 * Math.sin(dist * 0.01);
else alpha = Math.max(0, (vw - dist) / (vw * 0.2) * 0.3);
dot.el.style.opacity = alpha;
// Reset when too far
if (dist > vw * 0.7) {
dot.x = cx + (Math.random() - 0.5) * 20;
dot.y = cy + (Math.random() - 0.5) * 20;
const a = Math.random() * Math.PI * 2;
dot.vx = Math.cos(a); dot.vy = Math.sin(a);
dot.life = 0;
}
});
// ── TUNNEL CARDS — slide down (as if we're moving forward, walls scroll past) ──
wallCards.forEach(card => {
card.offset += spd * card.speedM * 0.8;
card.el.style.transform = `translateY(${card.offset}px)`;
// Cards drifting up into the distance or passing below
// Apply scale: farther away = smaller
const distFromCenter = card.offset; // positive = below center (passing by)
const absDist = Math.abs(distFromCenter);
const scale = Math.max(0.3, 1 - absDist / 600);
const opacity = Math.max(0.15, 1 - absDist / 500);
card.el.style.opacity = opacity;
card.el.style.transform += ` scale(${scale})`;
// Reset: when card scrolls off bottom, spawn new at top
if (card.offset > vh * 0.7) {
card.offset = -100 - Math.random() * 200;
// Pick random new data
const d = CARD_DATA[Math.floor(Math.random() * CARD_DATA.length)];
card.el.querySelector('.tl').textContent = d.label;
card.el.querySelector('.tv').textContent = d.value;
card.el.querySelector('.tv').className = 'tv ' + d.clr;
card.el.querySelector('.td').textContent = (d.dir === 'up' ? '↑ ' : '↓ ') + d.trend;
card.el.querySelector('.td').className = 'td ' + d.dir;
}
});
// ── GOO — glow responds to speed ──
const glowScale = 1 + spd * 0.2;
const glowOpacity = 0.5 + spd * 0.5;
goo.style.transform = `translate(-50%, -50%) scale(${glowScale})`;
goo.style.opacity = glowOpacity;
requestAnimationFrame(loop);
}
// ── SPEED CONTROL ──
function setSpeed(active) {
targetSpeed = active ? 1 : 0;
}
// ── INIT ──
initWalls();
requestAnimationFrame(loop);
window.addEventListener('resize', () => {
initWalls();
});
</script>
</body>
</html>