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
+112
-224
+112
-224
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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 href="https://fonts.cdnfonts.com/css/inter" rel="stylesheet">
|
||||
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet">
|
||||
@@ -34,30 +34,28 @@ body {
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PERSPECTIVE STAGE — the tunnel
|
||||
STAGE
|
||||
============================================================ */
|
||||
.stage {
|
||||
position: relative; width: 100%; height: 100vh;
|
||||
overflow: hidden;
|
||||
perspective: 1000px;
|
||||
perspective-origin: 50% 50%;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
VISUAL EFFECTS LAYER
|
||||
FX LAYER
|
||||
============================================================ */
|
||||
.fx-layer {
|
||||
position: absolute; inset: 0; pointer-events: none; z-index: 1;
|
||||
}
|
||||
|
||||
/* Noise texture */
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Goo — ambient light ahead */
|
||||
/* ── Glow at center ── */
|
||||
.goo {
|
||||
position: absolute; top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
@@ -70,58 +68,7 @@ body {
|
||||
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 lines from center ── */
|
||||
.radial-line {
|
||||
position: absolute; top: 50%; left: 50%;
|
||||
height: 1px;
|
||||
@@ -133,26 +80,52 @@ body {
|
||||
);
|
||||
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.3) 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);
|
||||
/* ============================================================
|
||||
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, no data, just headline
|
||||
CENTER — clean headline
|
||||
============================================================ */
|
||||
.center {
|
||||
position: absolute; top: 50%; left: 50%;
|
||||
@@ -217,15 +190,13 @@ body {
|
||||
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; }
|
||||
.orbit-card { width: 150px; padding: 10px 12px; }
|
||||
.orbit-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; }
|
||||
.orbit-card { width: 120px; padding: 8px 10px; }
|
||||
.orbit-card .tv { font-size: 13px; }
|
||||
.orbit-card .tl { font-size: 8px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -233,16 +204,12 @@ 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>
|
||||
<!-- Orbit cards injected by JS -->
|
||||
|
||||
<!-- CENTER -->
|
||||
<div class="center">
|
||||
<div class="badge">
|
||||
<span class="badge-dot"></span>
|
||||
@@ -271,81 +238,64 @@ body {
|
||||
|
||||
<script>
|
||||
/* ============================================================
|
||||
PERSPECTIVE TUNNEL ENGINE
|
||||
Cards on walls slide as we "drive forward"
|
||||
Radial lines from center outward
|
||||
Dots scatter like starfield
|
||||
ORBITAL ENGINE
|
||||
Cards rotate around center on elliptical paths
|
||||
============================================================ */
|
||||
|
||||
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 ──
|
||||
// ── SPEED ──
|
||||
let targetSpeed = 0;
|
||||
let currentSpeed = 0; // lerped: 0=cruise, 1=boost
|
||||
const CRUISE = 0.5; // always some motion
|
||||
const BOOST = 2.5;
|
||||
let currentSpeed = 0;
|
||||
const CRUISE = 0.4;
|
||||
const BOOST = 3.0;
|
||||
|
||||
// ── CARD POOL ──
|
||||
// ── CARD DATA ──
|
||||
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' },
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
// Track each card instance on the wall
|
||||
let wallCards = []; // { el, wall, offset, speedM, opacity }
|
||||
// ── ORBIT CARDS ──
|
||||
let orbitCards = [];
|
||||
// Each: { el, angle, orbitRx, orbitRy, yOffset, data, visible }
|
||||
|
||||
function spawnCard(data, offsetY) {
|
||||
function createOrbitCards() {
|
||||
CARD_DATA.forEach((d, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'tunnel-card';
|
||||
div.className = 'orbit-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>
|
||||
<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>
|
||||
`;
|
||||
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 };
|
||||
}
|
||||
stage.appendChild(div);
|
||||
|
||||
// Initialize walls with staggered cards
|
||||
function initWalls() {
|
||||
wallLeft.innerHTML = '';
|
||||
wallRight.innerHTML = '';
|
||||
wallCards = [];
|
||||
// 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;
|
||||
|
||||
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));
|
||||
orbitCards.push({
|
||||
el: div, angle, orbitRx, orbitRy, yOffset,
|
||||
data: d,
|
||||
speedMul: 0.7 + Math.random() * 0.6,
|
||||
});
|
||||
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 }
|
||||
|
||||
let radialLines = [];
|
||||
function spawnRadialLine() {
|
||||
const div = document.createElement('div');
|
||||
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 };
|
||||
}
|
||||
|
||||
// Initialize radial lines
|
||||
for (let i = 0; i < 50; i++) {
|
||||
for (let i = 0; i < 60; 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 spd = currentSpeed;
|
||||
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const cx = vw / 2;
|
||||
const cy = vh / 2;
|
||||
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) / 80);
|
||||
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)`;
|
||||
|
||||
// Reset
|
||||
if (dist > 90) {
|
||||
if (dist > 80) {
|
||||
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';
|
||||
// ── ORBIT CARDS — circular motion around center ──
|
||||
orbitCards.forEach(card => {
|
||||
card.angle += spd * card.speedMul * 0.015;
|
||||
|
||||
const dx = dot.x - cx;
|
||||
const dy = dot.y - cy;
|
||||
const dist = Math.sqrt(dx*dx + dy*dy);
|
||||
const a = card.angle;
|
||||
const rx = card.orbitRx;
|
||||
const ry = card.orbitRy;
|
||||
|
||||
// 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;
|
||||
// Position on ellipse
|
||||
const x = cx + rx * Math.cos(a);
|
||||
const y = cy + ry * Math.sin(a) + card.yOffset;
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
// 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
|
||||
|
||||
// ── 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)`;
|
||||
// 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;
|
||||
|
||||
// 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.left = x + 'px';
|
||||
card.el.style.top = y + 'px';
|
||||
card.el.style.transform = `translate(-50%, -50%) scale(${scale})`;
|
||||
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 glowOpacity = 0.5 + spd * 0.5;
|
||||
const glowOpacity = 0.5 + spd * 0.4;
|
||||
goo.style.transform = `translate(-50%, -50%) scale(${glowScale})`;
|
||||
goo.style.opacity = glowOpacity;
|
||||
|
||||
@@ -491,11 +379,11 @@ function setSpeed(active) {
|
||||
}
|
||||
|
||||
// ── INIT ──
|
||||
initWalls();
|
||||
createOrbitCards();
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
initWalls();
|
||||
// Cards will naturally adjust to new center on next frame
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in new issue
Block a user