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
This commit is contained in:
1 parent
2605318513
commit
f0c6b50fdd
1 file changed
+116
-228
+116
-228
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>ROI24 — Perspective Hero</title>
|
<title>ROI24 — Orbital Hero</title>
|
||||||
<link rel="preconnect" href="https://fonts.cdnfonts.com">
|
<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/inter" rel="stylesheet">
|
||||||
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet">
|
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet">
|
||||||
@@ -34,30 +34,28 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
PERSPECTIVE STAGE — the tunnel
|
STAGE
|
||||||
============================================================ */
|
============================================================ */
|
||||||
.stage {
|
.stage {
|
||||||
position: relative; width: 100%; height: 100vh;
|
position: relative; width: 100%; height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
perspective: 1000px;
|
|
||||||
perspective-origin: 50% 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
VISUAL EFFECTS LAYER
|
FX LAYER
|
||||||
============================================================ */
|
============================================================ */
|
||||||
.fx-layer {
|
.fx-layer {
|
||||||
position: absolute; inset: 0; pointer-events: none; z-index: 1;
|
position: absolute; inset: 0; pointer-events: none; z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Noise texture */
|
/* Noise */
|
||||||
.fx-layer::before {
|
.fx-layer::before {
|
||||||
content: ''; position: absolute; inset: 0;
|
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");
|
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;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Goo — ambient light ahead */
|
/* ── Glow at center ── */
|
||||||
.goo {
|
.goo {
|
||||||
position: absolute; top: 50%; left: 50%;
|
position: absolute; top: 50%; left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
@@ -70,58 +68,7 @@ body {
|
|||||||
transition: transform 0.5s ease, opacity 0.5s ease;
|
transition: transform 0.5s ease, opacity 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ── Radial lines from center ── */
|
||||||
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 {
|
.radial-line {
|
||||||
position: absolute; top: 50%; left: 50%;
|
position: absolute; top: 50%; left: 50%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@@ -133,26 +80,52 @@ body {
|
|||||||
);
|
);
|
||||||
transform-origin: left center;
|
transform-origin: left center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radial-line.fast {
|
.radial-line.fast {
|
||||||
background: linear-gradient(90deg,
|
background: linear-gradient(90deg,
|
||||||
rgba(255,255,255,0.15) 0%,
|
rgba(255,255,255,0.15) 0%,
|
||||||
rgba(34,197,94,0.35) 25%,
|
rgba(34,197,94,0.3) 25%,
|
||||||
rgba(34,197,94,0.08) 55%,
|
rgba(34,197,94,0.08) 55%,
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
height: 1.5px;
|
height: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── DOTS — starfield particles ── */
|
/* ============================================================
|
||||||
.dot {
|
ORBITING CARDS
|
||||||
position: absolute; border-radius: 50%;
|
============================================================ */
|
||||||
background: var(--accent);
|
.orbit-card {
|
||||||
box-shadow: 0 0 4px var(--accent);
|
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, no data, just headline
|
CENTER — clean headline
|
||||||
============================================================ */
|
============================================================ */
|
||||||
.center {
|
.center {
|
||||||
position: absolute; top: 50%; left: 50%;
|
position: absolute; top: 50%; left: 50%;
|
||||||
@@ -217,15 +190,13 @@ body {
|
|||||||
RESPONSIVE
|
RESPONSIVE
|
||||||
============================================================ */
|
============================================================ */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.wall-left { left: 1%; }
|
.orbit-card { width: 150px; padding: 10px 12px; }
|
||||||
.wall-right { right: 1%; }
|
.orbit-card .tv { font-size: 15px; }
|
||||||
.tunnel-card { width: 140px; padding: 10px 12px; }
|
|
||||||
.tunnel-card .tv { font-size: 15px; }
|
|
||||||
}
|
}
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.tunnel-card { width: 110px; padding: 8px 10px; }
|
.orbit-card { width: 120px; padding: 8px 10px; }
|
||||||
.tunnel-card .tv { font-size: 13px; }
|
.orbit-card .tv { font-size: 13px; }
|
||||||
.tunnel-card .tl { font-size: 8px; }
|
.orbit-card .tl { font-size: 8px; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -233,16 +204,12 @@ body {
|
|||||||
|
|
||||||
<div class="stage" id="stage">
|
<div class="stage" id="stage">
|
||||||
|
|
||||||
<!-- FX LAYER -->
|
|
||||||
<div class="fx-layer" id="fxLayer">
|
<div class="fx-layer" id="fxLayer">
|
||||||
<div class="goo" id="goo"></div>
|
<div class="goo" id="goo"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TUNNEL WALLS -->
|
<!-- Orbit cards injected by JS -->
|
||||||
<div class="wall wall-left" id="wallLeft"></div>
|
|
||||||
<div class="wall wall-right" id="wallRight"></div>
|
|
||||||
|
|
||||||
<!-- CENTER -->
|
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<div class="badge">
|
<div class="badge">
|
||||||
<span class="badge-dot"></span>
|
<span class="badge-dot"></span>
|
||||||
@@ -271,81 +238,64 @@ body {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
PERSPECTIVE TUNNEL ENGINE
|
ORBITAL ENGINE
|
||||||
Cards on walls slide as we "drive forward"
|
Cards rotate around center on elliptical paths
|
||||||
Radial lines from center outward
|
|
||||||
Dots scatter like starfield
|
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const stage = document.getElementById('stage');
|
const stage = document.getElementById('stage');
|
||||||
const wallLeft = document.getElementById('wallLeft');
|
|
||||||
const wallRight = document.getElementById('wallRight');
|
|
||||||
const fxLayer = document.getElementById('fxLayer');
|
const fxLayer = document.getElementById('fxLayer');
|
||||||
const goo = document.getElementById('goo');
|
const goo = document.getElementById('goo');
|
||||||
|
|
||||||
// ── SPEED STATE ──
|
// ── SPEED ──
|
||||||
let targetSpeed = 0;
|
let targetSpeed = 0;
|
||||||
let currentSpeed = 0; // lerped: 0=cruise, 1=boost
|
let currentSpeed = 0;
|
||||||
const CRUISE = 0.5; // always some motion
|
const CRUISE = 0.4;
|
||||||
const BOOST = 2.5;
|
const BOOST = 3.0;
|
||||||
|
|
||||||
// ── CARD POOL ──
|
// ── CARD DATA ──
|
||||||
const CARD_DATA = [
|
const CARD_DATA = [
|
||||||
{ label: 'Выручка за 30 дн', value: '2 051 857 ₽', trend: '+15%', clr: 'g', dir: 'up', wall: 'left' },
|
{ label: 'Выручка за 30 дн', value: '2 051 857 ₽', trend: '+15%', clr: 'g', dir: 'up' },
|
||||||
{ label: 'CPA средний', value: '890 ₽', trend: '−5%', clr: 'b', dir: 'down', wall: 'left' },
|
{ label: 'CPA средний', value: '890 ₽', trend: '−5%', clr: 'b', dir: 'down' },
|
||||||
{ label: 'Продажи', value: '1 247 шт', trend: '+12%', clr: 'b', dir: 'up', wall: 'left' },
|
{ label: 'Продажи', value: '1 247 шт', trend: '+12%', clr: 'b', dir: 'up' },
|
||||||
{ label: 'ROI рекламы', value: '91.8%', trend: '+5%', clr: 'a', dir: 'up', wall: 'right' },
|
{ label: 'ROI рекламы', value: '91.8%', trend: '+5%', clr: 'a', dir: 'up' },
|
||||||
{ label: 'Прибыль', value: '981 857 ₽', trend: '+12%', clr: 'g', dir: 'up', wall: 'right' },
|
{ label: 'Прибыль', value: '981 857 ₽', trend: '+12%', clr: 'g', dir: 'up' },
|
||||||
{ label: 'Конверсия', value: '2.8%', trend: '+0.3%',clr: 'a', dir: 'up', wall: 'right' },
|
{ 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' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Track each card instance on the wall
|
// ── ORBIT CARDS ──
|
||||||
let wallCards = []; // { el, wall, offset, speedM, opacity }
|
let orbitCards = [];
|
||||||
|
// Each: { el, angle, orbitRx, orbitRy, yOffset, data, visible }
|
||||||
|
|
||||||
function spawnCard(data, offsetY) {
|
function createOrbitCards() {
|
||||||
const div = document.createElement('div');
|
CARD_DATA.forEach((d, i) => {
|
||||||
div.className = 'tunnel-card';
|
const div = document.createElement('div');
|
||||||
div.innerHTML = `
|
div.className = 'orbit-card';
|
||||||
<div class="tl">${data.label}</div>
|
div.innerHTML = `
|
||||||
<div class="tv ${data.clr}">${data.value}</div>
|
<div class="tl">${d.label}</div>
|
||||||
<span class="td ${data.dir}">${data.dir === 'up' ? '↑' : '↓'} ${data.trend}</span>
|
<div class="tv ${d.clr}">${d.value}</div>
|
||||||
`;
|
<span class="td ${d.dir}">${d.dir === 'up' ? '↑' : '↓'} ${d.trend}</span>
|
||||||
const wall = data.wall === 'left' ? wallLeft : wallRight;
|
`;
|
||||||
wall.appendChild(div);
|
stage.appendChild(div);
|
||||||
return { el: div, wall: data.wall, offset: offsetY, speedM: 0.8 + Math.random() * 1.2 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize walls with staggered cards
|
// Distribute cards evenly around the orbit
|
||||||
function initWalls() {
|
const angle = (i / CARD_DATA.length) * Math.PI * 2;
|
||||||
wallLeft.innerHTML = '';
|
// Elliptical orbit: wider than tall
|
||||||
wallRight.innerHTML = '';
|
const orbitRx = 320 + Math.random() * 60; // horizontal radius
|
||||||
wallCards = [];
|
const orbitRy = 100 + Math.random() * 40; // vertical radius
|
||||||
|
const yOffset = (Math.random() - 0.5) * 60;
|
||||||
|
|
||||||
const leftData = CARD_DATA.filter(d => d.wall === 'left');
|
orbitCards.push({
|
||||||
const rightData = CARD_DATA.filter(d => d.wall === 'right');
|
el: div, angle, orbitRx, orbitRy, yOffset,
|
||||||
|
data: d,
|
||||||
// Stagger vertically: some visible, some ready to slide in
|
speedMul: 0.7 + Math.random() * 0.6,
|
||||||
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 ──
|
// ── RADIAL LINES ──
|
||||||
let radialLines = []; // { el, angle, length, speedM, life }
|
let radialLines = [];
|
||||||
|
|
||||||
function spawnRadialLine() {
|
function spawnRadialLine() {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
const isFast = Math.random() < 0.2;
|
const isFast = Math.random() < 0.2;
|
||||||
@@ -359,126 +309,64 @@ function spawnRadialLine() {
|
|||||||
return { el: div, angle, length, speedM: 0.3 + Math.random() * 1.0, life: 0 };
|
return { el: div, angle, length, speedM: 0.3 + Math.random() * 1.0, life: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize radial lines
|
for (let i = 0; i < 60; i++) {
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
radialLines.push(spawnRadialLine());
|
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 ──
|
// ── LOOP ──
|
||||||
function loop() {
|
function loop() {
|
||||||
const lerp = 0.04;
|
const lerp = 0.04;
|
||||||
currentSpeed += (CRUISE + targetSpeed * BOOST - currentSpeed) * lerp;
|
currentSpeed += (CRUISE + targetSpeed * BOOST - currentSpeed) * lerp;
|
||||||
const spd = currentSpeed; // shorthand
|
const spd = currentSpeed;
|
||||||
|
|
||||||
const vw = window.innerWidth;
|
const cx = window.innerWidth / 2;
|
||||||
const vh = window.innerHeight;
|
const cy = window.innerHeight / 2;
|
||||||
const cx = vw / 2;
|
|
||||||
const cy = vh / 2;
|
|
||||||
|
|
||||||
// ── RADIAL LINES ──
|
// ── RADIAL LINES ──
|
||||||
radialLines.forEach(line => {
|
radialLines.forEach(line => {
|
||||||
line.life += spd * line.speedM * 1.5;
|
line.life += spd * line.speedM * 1.5;
|
||||||
const dist = line.life;
|
const dist = line.life;
|
||||||
const alpha = dist < 10 ? dist / 10 : Math.max(0, 1 - (dist - 10) / 80);
|
const alpha = dist < 10 ? dist / 10 : Math.max(0, 1 - (dist - 10) / 70);
|
||||||
line.el.style.opacity = alpha;
|
line.el.style.opacity = alpha;
|
||||||
line.el.style.transform = `rotate(${line.angle}deg) translateX(${dist}px)`;
|
line.el.style.transform = `rotate(${line.angle}deg) translateX(${dist}px)`;
|
||||||
|
if (dist > 80) {
|
||||||
// Reset
|
|
||||||
if (dist > 90) {
|
|
||||||
line.life = 0;
|
line.life = 0;
|
||||||
line.angle = Math.random() * 360;
|
line.angle = Math.random() * 360;
|
||||||
line.el.style.transform = `rotate(${line.angle}deg) translateX(0px)`;
|
line.el.style.transform = `rotate(${line.angle}deg) translateX(0px)`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── DOTS (starfield) ──
|
// ── ORBIT CARDS — circular motion around center ──
|
||||||
dots.forEach(dot => {
|
orbitCards.forEach(card => {
|
||||||
dot.life += spd * dot.speedM;
|
card.angle += spd * card.speedMul * 0.015;
|
||||||
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 a = card.angle;
|
||||||
const dy = dot.y - cy;
|
const rx = card.orbitRx;
|
||||||
const dist = Math.sqrt(dx*dx + dy*dy);
|
const ry = card.orbitRy;
|
||||||
|
|
||||||
// Fade in near center, bright at mid distance, fade out at edges
|
// Position on ellipse
|
||||||
let alpha;
|
const x = cx + rx * Math.cos(a);
|
||||||
if (dist < 20) alpha = dist / 20 * 0.6;
|
const y = cy + ry * Math.sin(a) + card.yOffset;
|
||||||
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
|
// Z-depth: cards in front (cos(a) > 0) are larger/brighter
|
||||||
if (dist > vw * 0.7) {
|
// Cards behind (cos(a) < 0) are faded
|
||||||
dot.x = cx + (Math.random() - 0.5) * 20;
|
const frontness = Math.cos(a); // 1 = right, -1 = left, but we want the "arc" visibility
|
||||||
dot.y = cy + (Math.random() - 0.5) * 20;
|
const depthFactor = Math.abs(Math.sin(a)); // 0 at left/right edges, 1 at top/bottom
|
||||||
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) ──
|
// Visibility: most visible when near the horizontal axis (left or right of center)
|
||||||
wallCards.forEach(card => {
|
const horizontalFactor = Math.abs(Math.cos(a)); // 1 at right/left, 0 at front/back
|
||||||
card.offset += spd * card.speedM * 0.8;
|
const opacity = Math.max(0.10, horizontalFactor * 0.9 + 0.1);
|
||||||
card.el.style.transform = `translateY(${card.offset}px)`;
|
const scale = 0.6 + horizontalFactor * 0.4;
|
||||||
|
|
||||||
// Cards drifting up into the distance or passing below
|
card.el.style.left = x + 'px';
|
||||||
// Apply scale: farther away = smaller
|
card.el.style.top = y + 'px';
|
||||||
const distFromCenter = card.offset; // positive = below center (passing by)
|
card.el.style.transform = `translate(-50%, -50%) scale(${scale})`;
|
||||||
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.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 ──
|
// ── GLOW ──
|
||||||
const glowScale = 1 + spd * 0.2;
|
const glowScale = 1 + spd * 0.2;
|
||||||
const glowOpacity = 0.5 + spd * 0.5;
|
const glowOpacity = 0.5 + spd * 0.4;
|
||||||
goo.style.transform = `translate(-50%, -50%) scale(${glowScale})`;
|
goo.style.transform = `translate(-50%, -50%) scale(${glowScale})`;
|
||||||
goo.style.opacity = glowOpacity;
|
goo.style.opacity = glowOpacity;
|
||||||
|
|
||||||
@@ -491,11 +379,11 @@ function setSpeed(active) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── INIT ──
|
// ── INIT ──
|
||||||
initWalls();
|
createOrbitCards();
|
||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
initWalls();
|
// Cards will naturally adjust to new center on next frame
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user