Files
roi24/hero-v4.html
T
neo f0c6b50fdd hero-v4: orbital cards — KPI cards rotate around center headline
Orbital engine:
- 8 KPI cards on elliptical orbits (rx:320-380, ry:100-140)
- Cards rotate around center — appear on left/right edges
- Visibility = |cos(angle)| — brightest at sides, fade toward center
- Scale: 0.6–1.0 based on horizontal position
- 60 radial lines from center outward (forward motion feel)
- Glow pulses & expands with speed
- Center: clean headline + CTA (composio.dev style)
- Hover = accelerate orbit speed (CRUISE 0.4 → BOOST 3.0)

No more dots, no falling snow — coherent orbital dance around the text
2026-06-21 21:30:59 +05:00

392 lines
13 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 — Orbital 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;
}
/* ============================================================
STAGE
============================================================ */
.stage {
position: relative; width: 100%; height: 100vh;
overflow: hidden;
}
/* ============================================================
FX LAYER
============================================================ */
.fx-layer {
position: absolute; inset: 0; pointer-events: none; z-index: 1;
}
/* Noise */
.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;
}
/* ── Glow at center ── */
.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;
}
/* ── Radial lines from center ── */
.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.3) 25%,
rgba(34,197,94,0.08) 55%,
transparent 100%
);
height: 1.5px;
}
/* ============================================================
ORBITING CARDS
============================================================ */
.orbit-card {
position: absolute;
background: rgba(22,22,24,0.88);
backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px;
padding: 12px 16px;
width: 190px;
pointer-events: none;
transition: opacity 0.2s ease;
}
.orbit-card .tl {
font-size: 9px; text-transform: uppercase; letter-spacing: 0.07em;
color: var(--text-dim); font-weight: 600; margin-bottom: 4px;
}
.orbit-card .tv {
font-family: var(--mono); font-size: 18px; font-weight: 700;
line-height: 1.15; margin-bottom: 3px;
}
.orbit-card .tv.g { color: var(--accent); }
.orbit-card .tv.b { color: var(--blue); }
.orbit-card .tv.a { color: var(--amber); }
.orbit-card .td {
display: inline-flex; align-items: center; gap: 3px;
padding: 2px 6px; border-radius: 4px;
font-family: var(--mono); font-weight: 600; font-size: 10px;
}
.orbit-card .td.up { color: var(--accent); background: var(--accent-dim); }
.orbit-card .td.down { color: var(--red); background: rgba(239,68,68,0.12); }
/* ============================================================
CENTER — clean 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) {
.orbit-card { width: 150px; padding: 10px 12px; }
.orbit-card .tv { font-size: 15px; }
}
@media (max-width: 480px) {
.orbit-card { width: 120px; padding: 8px 10px; }
.orbit-card .tv { font-size: 13px; }
.orbit-card .tl { font-size: 8px; }
}
</style>
</head>
<body>
<div class="stage" id="stage">
<div class="fx-layer" id="fxLayer">
<div class="goo" id="goo"></div>
</div>
<!-- Orbit cards injected by JS -->
<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>
/* ============================================================
ORBITAL ENGINE
Cards rotate around center on elliptical paths
============================================================ */
const stage = document.getElementById('stage');
const fxLayer = document.getElementById('fxLayer');
const goo = document.getElementById('goo');
// ── SPEED ──
let targetSpeed = 0;
let currentSpeed = 0;
const CRUISE = 0.4;
const BOOST = 3.0;
// ── CARD DATA ──
const CARD_DATA = [
{ label: 'Выручка за 30 дн', value: '2 051 857 ₽', trend: '+15%', clr: 'g', dir: 'up' },
{ label: 'CPA средний', value: '890 ₽', trend: '−5%', clr: 'b', dir: 'down' },
{ label: 'Продажи', value: '1 247 шт', trend: '+12%', clr: 'b', dir: 'up' },
{ label: 'ROI рекламы', value: '91.8%', trend: '+5%', clr: 'a', dir: 'up' },
{ label: 'Прибыль', value: '981 857 ₽', trend: '+12%', clr: 'g', dir: 'up' },
{ label: 'Конверсия', value: '2.8%', trend: '+0.3%',clr: 'a', dir: 'up' },
{ label: 'LTV клиента', value: '12 400 ₽', trend: '+18%', clr: 'g', dir: 'up' },
{ label: 'Ср. чек', value: '1 850 ₽', trend: '−2%', clr: 'b', dir: 'down' },
];
// ── ORBIT CARDS ──
let orbitCards = [];
// Each: { el, angle, orbitRx, orbitRy, yOffset, data, visible }
function createOrbitCards() {
CARD_DATA.forEach((d, i) => {
const div = document.createElement('div');
div.className = 'orbit-card';
div.innerHTML = `
<div class="tl">${d.label}</div>
<div class="tv ${d.clr}">${d.value}</div>
<span class="td ${d.dir}">${d.dir === 'up' ? '↑' : '↓'} ${d.trend}</span>
`;
stage.appendChild(div);
// Distribute cards evenly around the orbit
const angle = (i / CARD_DATA.length) * Math.PI * 2;
// Elliptical orbit: wider than tall
const orbitRx = 320 + Math.random() * 60; // horizontal radius
const orbitRy = 100 + Math.random() * 40; // vertical radius
const yOffset = (Math.random() - 0.5) * 60;
orbitCards.push({
el: div, angle, orbitRx, orbitRy, yOffset,
data: d,
speedMul: 0.7 + Math.random() * 0.6,
});
});
}
// ── RADIAL LINES ──
let radialLines = [];
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 };
}
for (let i = 0; i < 60; i++) {
radialLines.push(spawnRadialLine());
}
// ── LOOP ──
function loop() {
const lerp = 0.04;
currentSpeed += (CRUISE + targetSpeed * BOOST - currentSpeed) * lerp;
const spd = currentSpeed;
const cx = window.innerWidth / 2;
const cy = window.innerHeight / 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) / 70);
line.el.style.opacity = alpha;
line.el.style.transform = `rotate(${line.angle}deg) translateX(${dist}px)`;
if (dist > 80) {
line.life = 0;
line.angle = Math.random() * 360;
line.el.style.transform = `rotate(${line.angle}deg) translateX(0px)`;
}
});
// ── ORBIT CARDS — circular motion around center ──
orbitCards.forEach(card => {
card.angle += spd * card.speedMul * 0.015;
const a = card.angle;
const rx = card.orbitRx;
const ry = card.orbitRy;
// Position on ellipse
const x = cx + rx * Math.cos(a);
const y = cy + ry * Math.sin(a) + card.yOffset;
// Z-depth: cards in front (cos(a) > 0) are larger/brighter
// Cards behind (cos(a) < 0) are faded
const frontness = Math.cos(a); // 1 = right, -1 = left, but we want the "arc" visibility
const depthFactor = Math.abs(Math.sin(a)); // 0 at left/right edges, 1 at top/bottom
// Visibility: most visible when near the horizontal axis (left or right of center)
const horizontalFactor = Math.abs(Math.cos(a)); // 1 at right/left, 0 at front/back
const opacity = Math.max(0.10, horizontalFactor * 0.9 + 0.1);
const scale = 0.6 + horizontalFactor * 0.4;
card.el.style.left = x + 'px';
card.el.style.top = y + 'px';
card.el.style.transform = `translate(-50%, -50%) scale(${scale})`;
card.el.style.opacity = opacity;
});
// ── GLOW ──
const glowScale = 1 + spd * 0.2;
const glowOpacity = 0.5 + spd * 0.4;
goo.style.transform = `translate(-50%, -50%) scale(${glowScale})`;
goo.style.opacity = glowOpacity;
requestAnimationFrame(loop);
}
// ── SPEED CONTROL ──
function setSpeed(active) {
targetSpeed = active ? 1 : 0;
}
// ── INIT ──
createOrbitCards();
requestAnimationFrame(loop);
window.addEventListener('resize', () => {
// Cards will naturally adjust to new center on next frame
});
</script>
</body>
</html>