/* ============================================= WEDDING SITE — АЛЕКСАНДР & ЮЛИЯ ============================================= */ // ---- Обратный отсчёт ---- const WEDDING_DATE = new Date('2026-08-22T16:00:00+03:00'); function updateCountdown() { const now = new Date(); const diff = WEDDING_DATE - now; if (diff <= 0) { document.getElementById('countdown').innerHTML = '
🎉 Сегодня наша свадьба!
'; return; } const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); document.getElementById('cd-days').textContent = String(days); document.getElementById('cd-hours').textContent = String(hours).padStart(2, '0'); document.getElementById('cd-minutes').textContent = String(minutes).padStart(2, '0'); document.getElementById('cd-seconds').textContent = String(seconds).padStart(2, '0'); } updateCountdown(); setInterval(updateCountdown, 1000); // ---- Конфетти ---- const CONFETTI_COLORS = ['#FFD166', '#06D6A0', '#9B5DE5', '#F15BB5', '#00BBF9', '#FF6B6B']; const CONFETTI_SHAPES = ['border-radius:2px', 'border-radius:50%', 'clip-path:polygon(50% 0%,0% 100%,100% 100%)']; // Фоновый дождь конфетти на герое (function spawnConfetti() { const container = document.getElementById('confetti'); for (let i = 0; i < 60; i++) { const el = document.createElement('div'); el.className = 'confetti-piece'; const size = Math.random() * 8 + 6; el.style.cssText = ` left: ${Math.random() * 100}%; width: ${size}px; height: ${size}px; background: ${CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)]}; ${CONFETTI_SHAPES[Math.floor(Math.random() * CONFETTI_SHAPES.length)]}; animation-duration: ${Math.random() * 6 + 5}s; animation-delay: ${Math.random() * 8}s; `; container.appendChild(el); } })(); // Взрыв конфетти из заданной точки function burstConfetti(x, y, count = 40) { for (let i = 0; i < count; i++) { const el = document.createElement('div'); el.className = 'confetti-burst'; const size = Math.random() * 9 + 5; const angle = Math.random() * 360; const dist = Math.random() * 160 + 60; const color = CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)]; const shape = CONFETTI_SHAPES[Math.floor(Math.random() * CONFETTI_SHAPES.length)]; const dur = Math.random() * 600 + 700; el.style.cssText = ` position: fixed; left: ${x}px; top: ${y}px; width: ${size}px; height: ${size}px; background: ${color}; ${shape}; pointer-events: none; z-index: 9999; transform: translate(-50%, -50%); animation: burst ${dur}ms ease-out forwards; --dx: ${Math.cos(angle * Math.PI / 180) * dist}px; --dy: ${Math.sin(angle * Math.PI / 180) * dist}px; `; document.body.appendChild(el); setTimeout(() => el.remove(), dur + 50); } } // Взрыв при клике в любом месте страницы document.addEventListener('click', (e) => { burstConfetti(e.clientX, e.clientY, 35); }); // ===== ДИСКО ШАР ===== (function initDiscoBall() { const BALL_R = 45; // радиус шара (в 3 раза больше прежнего) const ROPE_LEN = 18; // верёвка от низа шапки до верха шара const WIRE_MAX = 26; // максимальная длина провода прожектора const TILE_S = 7; // размер зеркального тайла const TILE_G = 1; // зазор между тайлами const LERP_SPD = 0.10; // скорость выезда/уезда прожектора (0..1) const DISC_COLORS = ['#ff6b9d','#ffd166','#06d6a0','#00bbf9','#9b5de5','#ffffff','#ff9f43','#a8e6cf']; const cvs = document.createElement('canvas'); cvs.id = 'disco-canvas'; cvs.style.cssText = 'position:fixed;top:0;left:0;pointer-events:none;z-index:998;'; document.body.appendChild(cvs); const ctx = cvs.getContext('2d'); let CW = window.innerWidth, CH = 280; cvs.width = CW; cvs.height = CH; let rot = 0; let glints = []; // разлетающиеся блики let ballPos = null; // Для каждой нав-ссылки храним прогресс выезда (0 = спрятан, 1 = полностью выехал) // и флаг — стреляли ли мы уже бликами при этом появлении const projState = new Map(); // link → { progress, target, glintFired } function refreshPos() { const logo = document.querySelector('.nav-logo'); const nav = document.getElementById('navbar'); if (!logo || !nav) return; const lr = logo.getBoundingClientRect(); const nh = nav.getBoundingClientRect().height; ballPos = { x: lr.left + lr.width / 2, ropeTopY: nh, ballY: nh + ROPE_LEN + BALL_R, navH: nh, }; } window.addEventListener('resize', () => { CW = cvs.width = window.innerWidth; refreshPos(); }); refreshPos(); // Инициализируем состояние для каждой ссылки document.querySelectorAll('.nav-links a').forEach(link => { projState.set(link, { progress: 0, target: 0, glintFired: false }); link.addEventListener('mouseenter', e => { const s = projState.get(e.currentTarget); if (s) { s.target = 1; } }); link.addEventListener('mouseleave', e => { const s = projState.get(e.currentTarget); if (s) { s.target = 0; s.glintFired = false; } }); }); // ---- корпус прожектора Fresnel в локальных координатах (линза смотрит вправо) ---- function drawFresnelBody() { const BW = 46, BH = 32, FW = 20, LR = 11; const FLAP_L = 22, FLAP_T = 5.5; ctx.shadowColor = 'rgba(0,0,0,.7)'; ctx.shadowBlur = 14; // ── Шторки-барндоры (рисуем ДО корпуса — они «выходят» из него) ─ // Верхняя шторка ctx.save(); ctx.translate(BW / 2 - FW * 0.5, -BH / 2); ctx.rotate(-0.52); ctx.fillStyle = '#161616'; ctx.strokeStyle = '#353535'; ctx.lineWidth = 0.7; ctx.beginPath(); ctx.rect(-3, -FLAP_T / 2, FLAP_L + 3, FLAP_T); ctx.fill(); ctx.stroke(); // Шарнир ctx.fillStyle = '#444'; ctx.strokeStyle = '#666'; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.arc(0, 0, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); // Рёбра жёсткости for (let i = 6; i < FLAP_L; i += 7) { ctx.strokeStyle = '#0e0e0e'; ctx.lineWidth = 0.6; ctx.beginPath(); ctx.moveTo(i, -FLAP_T / 2 + 1); ctx.lineTo(i, FLAP_T / 2 - 1); ctx.stroke(); } ctx.restore(); // Нижняя шторка ctx.save(); ctx.translate(BW / 2 - FW * 0.5, BH / 2); ctx.rotate(0.52); ctx.fillStyle = '#161616'; ctx.strokeStyle = '#353535'; ctx.lineWidth = 0.7; ctx.beginPath(); ctx.rect(-3, -FLAP_T / 2, FLAP_L + 3, FLAP_T); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#444'; ctx.strokeStyle = '#666'; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.arc(0, 0, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); for (let i = 6; i < FLAP_L; i += 7) { ctx.strokeStyle = '#0e0e0e'; ctx.lineWidth = 0.6; ctx.beginPath(); ctx.moveTo(i, -FLAP_T / 2 + 1); ctx.lineTo(i, FLAP_T / 2 - 1); ctx.stroke(); } ctx.restore(); // Боковая шторка (правая, видна при боковом ракурсе) ctx.save(); ctx.translate(BW / 2, 0); ctx.rotate(-0.14); ctx.fillStyle = '#1a1a1a'; ctx.strokeStyle = '#2e2e2e'; ctx.lineWidth = 0.7; ctx.beginPath(); ctx.rect(0, -BH / 2 - 1, FLAP_L * 0.55, BH + 2); ctx.fill(); ctx.stroke(); ctx.restore(); // ── Основной корпус ─────────────────────────────────────────── // Внешний кант (тень) ctx.fillStyle = '#070707'; ctx.fillRect(-BW / 2 - 1.5, -BH / 2 - 1.5, BW + 3, BH + 3); // Корпус с градиентом const bg = ctx.createLinearGradient(-BW / 2, -BH / 2, -BW / 2, BH / 2); bg.addColorStop(0, '#2f2f2f'); bg.addColorStop(0.42, '#1d1d1d'); bg.addColorStop(1, '#111'); ctx.fillStyle = bg; ctx.fillRect(-BW / 2, -BH / 2, BW, BH); // Металлическая фаска сверху ctx.fillStyle = 'rgba(255,255,255,.08)'; ctx.fillRect(-BW / 2, -BH / 2, BW, 2.5); // Тень снизу ctx.fillStyle = 'rgba(0,0,0,.18)'; ctx.fillRect(-BW / 2, BH / 2 - 2.5, BW, 2.5); // Задняя затемнённая секция (радиатор охлаждения) ctx.fillStyle = 'rgba(0,0,0,.28)'; ctx.fillRect(-BW / 2, -BH / 2, BW * 0.22, BH); // Рёбра радиатора ctx.strokeStyle = '#0c0c0c'; ctx.lineWidth = 1.2; for (let i = 0; i < 3; i++) { const sx = -BW / 2 + 4 + i * 4.5; ctx.beginPath(); ctx.moveTo(sx, -BH / 2 + 5); ctx.lineTo(sx, BH / 2 - 5); ctx.stroke(); } // Вентиляционные прорези (средняя часть корпуса) ctx.strokeStyle = '#0a0a0a'; ctx.lineWidth = 1.8; for (let i = 0; i < 4; i++) { const sx = -BW / 2 + 17 + i * 6; if (sx > BW / 2 - FW - 5) break; ctx.beginPath(); ctx.moveTo(sx, -BH / 2 + 6); ctx.lineTo(sx, BH / 2 - 6); ctx.stroke(); } // Логотип / шильдик на корпусе ctx.fillStyle = 'rgba(255,255,255,.04)'; ctx.fillRect(-BW / 2 + 18, BH / 2 - 9, 14, 5); // Ручка/рукоятка сверху корпуса ctx.fillStyle = '#252525'; ctx.strokeStyle = '#3e3e3e'; ctx.lineWidth = 0.9; ctx.beginPath(); ctx.rect(-10, -BH / 2 - 10, 20, 10); ctx.fill(); ctx.stroke(); // Насечки на ручке ctx.strokeStyle = '#4a4a4a'; ctx.lineWidth = 0.55; for (let i = -7; i <= 7; i += 3.5) { ctx.beginPath(); ctx.moveTo(i, -BH / 2 - 9); ctx.lineTo(i, -BH / 2 - 2); ctx.stroke(); } // ── Передняя секция (корпус линзы + рамка шторок) ───────────── ctx.fillStyle = '#0d0d0d'; ctx.fillRect(BW / 2 - FW, -BH / 2, FW, BH); // Квадратный фланец под шторки ctx.strokeStyle = '#2c2c2c'; ctx.lineWidth = 2; ctx.strokeRect(BW / 2 - FW + 2, -BH / 2 + 2, FW - 4, BH - 4); // Болты фланца [ [BW / 2 - FW + 5, -BH / 2 + 5], [BW / 2 - FW + 5, BH / 2 - 5], [BW / 2 - 4, -BH / 2 + 5], [BW / 2 - 4, BH / 2 - 5], ].forEach(([bx2, by2]) => { ctx.beginPath(); ctx.arc(bx2, by2, 1.8, 0, Math.PI * 2); ctx.fillStyle = '#404040'; ctx.fill(); ctx.strokeStyle = '#666'; ctx.lineWidth = 0.5; ctx.stroke(); // Крест болта ctx.strokeStyle = '#555'; ctx.lineWidth = 0.4; ctx.beginPath(); ctx.moveTo(bx2 - 1.2, by2); ctx.lineTo(bx2 + 1.2, by2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(bx2, by2 - 1.2); ctx.lineTo(bx2, by2 + 1.2); ctx.stroke(); }); // ── Линза Fresnel ───────────────────────────────────────────── const lx = BW / 2 - FW / 2, ly = 0; // Чёрная оправа линзы ctx.beginPath(); ctx.arc(lx, ly, LR + 3.5, 0, Math.PI * 2); ctx.fillStyle = '#060606'; ctx.fill(); ctx.strokeStyle = '#424242'; ctx.lineWidth = 1.3; ctx.stroke(); // Внутренняя тень оправы ctx.beginPath(); ctx.arc(lx, ly, LR + 2, 0, Math.PI * 2); ctx.strokeStyle = '#1a1a1a'; ctx.lineWidth = 1; ctx.stroke(); // Концентрические кольца Fresnel (физическая текстура линзы) for (let ri = LR - 0.5; ri > 1.5; ri -= 2.3) { ctx.beginPath(); ctx.arc(lx, ly, ri, 0, Math.PI * 2); const alpha = 0.05 + (LR - ri) * 0.05; ctx.strokeStyle = `rgba(255,200,60,${alpha})`; ctx.lineWidth = 1; ctx.stroke(); } // Свечение линзы — тёплый вольфрамовый цвет const lg = ctx.createRadialGradient(lx - 2.5, ly - 2.5, 0, lx, ly, LR + 3.5); lg.addColorStop(0, 'rgba(255,255,230,1)'); lg.addColorStop(0.22, 'rgba(255,230,140,.97)'); lg.addColorStop(0.55, 'rgba(255,150,20,.78)'); lg.addColorStop(0.82, 'rgba(210,60,0,.32)'); lg.addColorStop(1, 'rgba(150,30,0,0)'); ctx.beginPath(); ctx.arc(lx, ly, LR + 3.5, 0, Math.PI * 2); ctx.fillStyle = lg; ctx.fill(); // Зеркальный блик на линзе ctx.beginPath(); ctx.arc(lx - 4, ly - 4, 3, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,.75)'; ctx.fill(); ctx.beginPath(); ctx.arc(lx - 3, ly - 3, 1, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,.95)'; ctx.fill(); // ── Вилка-держатель (yoke / U-кронштейн) ───────────────────── ctx.shadowBlur = 0; ctx.strokeStyle = '#3c3c3c'; ctx.lineWidth = 3.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); ctx.moveTo(-BW / 4 + 2, -BH / 2 - 2); ctx.lineTo(-BW / 4 + 2, -BH / 2 - 15); ctx.lineTo( BW / 4 - 2, -BH / 2 - 15); ctx.lineTo( BW / 4 - 2, -BH / 2 - 2); ctx.stroke(); // Скоба кронштейна соединяется горизонтально с креплением ctx.strokeStyle = '#4a4a4a'; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(-BW / 4 + 2, -BH / 2 - 15); ctx.lineTo( BW / 4 - 2, -BH / 2 - 15); ctx.stroke(); // Шарнирный болт (точка поворота прожектора в кронштейне) ctx.beginPath(); ctx.arc(0, -BH / 2 - 15, 4.5, 0, Math.PI * 2); ctx.fillStyle = '#5a5a5a'; ctx.fill(); ctx.strokeStyle = '#888'; ctx.lineWidth = 0.9; ctx.stroke(); ctx.strokeStyle = '#777'; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(-2.5, -BH / 2 - 15); ctx.lineTo(2.5, -BH / 2 - 15); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, -BH / 2 - 17.5); ctx.lineTo(0, -BH / 2 - 12.5); ctx.stroke(); ctx.shadowBlur = 0; ctx.lineCap = 'butt'; } // ---- рисуем прожектор с учётом прогресса (0..1) ---- function drawSpotlight(link, progress) { if (progress < 0.01) return; const r = link.getBoundingClientRect(); const px = r.left + r.width / 2; const wireY0 = ballPos.navH; const wireLen = WIRE_MAX * progress; const projX = px; const projY = wireY0 + wireLen; const bx = ballPos.x, by = ballPos.ballY; // Угол от центра прожектора к диско-шару const angle = Math.atan2(by - projY, bx - projX); // Позиция линзы в мировых координатах const BW = 46, FW = 20; const lensLocalX = BW / 2 - FW / 2; const lensWX = projX + Math.cos(angle) * lensLocalX; const lensWY = projY + Math.sin(angle) * lensLocalX; ctx.save(); ctx.globalAlpha = Math.pow(progress, 0.65); // ── Конус света (рисуем ДО корпуса, чтобы корпус был поверх) ── const dx = bx - lensWX, dy = by - lensWY; const beamLen = Math.sqrt(dx * dx + dy * dy); const beamAng = Math.atan2(dy, dx); const ha = 0.20; const beam = ctx.createLinearGradient(lensWX, lensWY, bx, by); beam.addColorStop(0, 'rgba(255,245,180,.60)'); beam.addColorStop(0.5, 'rgba(255,230,120,.15)'); beam.addColorStop(1, 'rgba(255,200,50,.02)'); ctx.beginPath(); ctx.moveTo(lensWX, lensWY); ctx.lineTo(lensWX + Math.cos(beamAng - ha) * beamLen, lensWY + Math.sin(beamAng - ha) * beamLen); ctx.lineTo(lensWX + Math.cos(beamAng + ha) * beamLen, lensWY + Math.sin(beamAng + ha) * beamLen); ctx.closePath(); ctx.fillStyle = beam; ctx.fill(); // ── Провод (прямо вниз, к центру корпуса) ──────────────────── ctx.strokeStyle = 'rgba(45,45,45,.92)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(px, wireY0); ctx.lineTo(projX, projY); ctx.stroke(); // ── Корпус прожектора ───────────────────────────────────────── ctx.translate(projX, projY); ctx.rotate(angle); drawFresnelBody(); ctx.restore(); } // ---- рисуем шар ---- function drawBall() { const cx = ballPos.x, cy = ballPos.ballY; // Верёвка ctx.save(); ctx.strokeStyle = 'rgba(110,85,60,.9)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cx, ballPos.ropeTopY); ctx.lineTo(cx, cy - BALL_R); ctx.stroke(); ctx.restore(); // Клип по кругу ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, BALL_R, 0, Math.PI * 2); ctx.clip(); // Металлическая основа const base = ctx.createRadialGradient(cx - BALL_R * 0.28, cy - BALL_R * 0.3, 0, cx, cy, BALL_R); base.addColorStop(0, '#e8e8e8'); base.addColorStop(0.35,'#999'); base.addColorStop(1, '#0e0e0e'); ctx.fillStyle = base; ctx.fillRect(cx - BALL_R, cy - BALL_R, BALL_R * 2, BALL_R * 2); // Суммарный прогресс всех прожекторов (0 = ни одного, 1+ = полностью активны) let totalProj = 0; projState.forEach(s => { totalProj += s.progress; }); const colorMix = Math.min(1, totalProj); // 0 = только белый, 1 = полные цвета // Вспомогательная функция: смешать hex-цвет с белым по коэффициенту t (0=белый, 1=цвет) function mixWithWhite(hex, t) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); const ri = Math.round(r + (255 - r) * (1 - t)); const gi = Math.round(g + (255 - g) * (1 - t)); const bi = Math.round(b + (255 - b) * (1 - t)); return `rgb(${ri},${gi},${bi})`; } // Зеркальные тайлы const step = TILE_S + TILE_G; for (let ty = -BALL_R; ty < BALL_R; ty += step) { for (let tx = -BALL_R; tx < BALL_R; tx += step) { if (tx * tx + ty * ty >= BALL_R * BALL_R) continue; const angle = Math.atan2(ty, tx); const dist = Math.sqrt(tx * tx + ty * ty) / BALL_R; const ci = ( (Math.floor((angle + rot * 2.5) / (Math.PI / 4)) % DISC_COLORS.length + DISC_COLORS.length) % DISC_COLORS.length + Math.floor(dist * 3) ) % DISC_COLORS.length; const bright = 0.18 + 0.82 * Math.max(0, Math.cos(angle - rot * 1.5)); const boost = Math.min(0.35, totalProj * 0.25); // Без прожектора — серебристо-белые тайлы; с прожектором — цветные const tileColor = colorMix < 0.01 ? `hsl(0,0%,${Math.round(55 + bright * 45)}%)` : mixWithWhite(DISC_COLORS[ci], colorMix); ctx.fillStyle = tileColor; ctx.globalAlpha = Math.min(1, bright + boost) * 0.90; ctx.fillRect(cx + tx, cy + ty, TILE_S, TILE_S); } } ctx.globalAlpha = 1; // Глянцевый блик (объём) const shine = ctx.createRadialGradient(cx - BALL_R * 0.32, cy - BALL_R * 0.38, 0, cx, cy, BALL_R); shine.addColorStop(0, 'rgba(255,255,255,.72)'); shine.addColorStop(0.25, 'rgba(255,255,255,.07)'); shine.addColorStop(1, 'rgba(0,0,0,.55)'); ctx.fillStyle = shine; ctx.fillRect(cx - BALL_R, cy - BALL_R, BALL_R * 2, BALL_R * 2); ctx.restore(); // Крепёжный колпачок ctx.save(); ctx.fillStyle = '#bbb'; ctx.beginPath(); ctx.arc(cx, cy - BALL_R + 3, 5, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } // ---- орбитальные блики (масштаб зависит от суммы прожекторов) ---- function drawOrbitGlints() { let totalProj = 0; projState.forEach(s => { totalProj += s.progress; }); if (totalProj < 0.05) return; const cx = ballPos.x, cy = ballPos.ballY; const count = 12; for (let i = 0; i < count; i++) { const a = rot * 4 + i * ((Math.PI * 2) / count); const r = BALL_R + 6 + 4 * Math.sin(rot * 5 + i * 1.1); const sx = cx + Math.cos(a) * r; const sy = cy + Math.sin(a) * r * 0.5; const al = totalProj * (0.3 + 0.7 * Math.abs(Math.sin(rot * 9 + i * 1.5))); ctx.beginPath(); ctx.arc(sx, sy, 2.2, 0, Math.PI * 2); ctx.fillStyle = DISC_COLORS[i % DISC_COLORS.length]; ctx.globalAlpha = Math.min(1, al); ctx.fill(); } ctx.globalAlpha = 1; } // ---- основной цикл ---- function loop() { ctx.clearRect(0, 0, CW, CH); rot += 0.007; if (!ballPos) { refreshPos(); requestAnimationFrame(loop); return; } // Обновляем прогресс каждого прожектора (плавный выезд/заезд) projState.forEach((s, link) => { s.progress += (s.target - s.progress) * LERP_SPD; // Стреляем бликами когда прожектор почти полностью выехал if (s.progress > 0.85 && !s.glintFired) { s.glintFired = true; for (let i = 0; i < 16; i++) { const a = Math.random() * Math.PI * 2; glints.push({ x: ballPos.x, y: ballPos.ballY, vx: Math.cos(a) * (Math.random() * 5 + 1.5), vy: Math.sin(a) * (Math.random() * 5 + 1.5) - 1.5, life: 1, decay: 0.022 + Math.random() * 0.025, color: DISC_COLORS[Math.floor(Math.random() * DISC_COLORS.length)], r: 1.5 + Math.random() * 2.5, }); } } // Рисуем прожектор с текущим прогрессом drawSpotlight(link, s.progress); }); // Шар на верёвке drawBall(); // Орбитальные блики (перед шаром) drawOrbitGlints(); // Разлетающиеся блики glints = glints.filter(g => g.life > 0); glints.forEach(g => { ctx.beginPath(); ctx.arc(g.x, g.y, g.r, 0, Math.PI * 2); ctx.fillStyle = g.color; ctx.globalAlpha = g.life; ctx.fill(); ctx.globalAlpha = 1; g.x += g.vx; g.y += g.vy; g.vy += 0.09; g.life -= g.decay; }); requestAnimationFrame(loop); } loop(); })(); // ---- Навигация: бургер-меню ---- const burger = document.getElementById('burger'); const navLinks = document.getElementById('nav-links'); burger.addEventListener('click', () => { navLinks.classList.toggle('open'); }); // Закрыть меню при клике на ссылку navLinks.querySelectorAll('a').forEach(link => { link.addEventListener('click', () => navLinks.classList.remove('open')); }); // ---- Навигация: тень при прокрутке ---- const navbar = document.getElementById('navbar'); window.addEventListener('scroll', () => { navbar.classList.toggle('scrolled', window.scrollY > 20); }); // ---- FAQ: аккордеон ---- function setFaqAnswerHeight(answer, open) { if (open) { answer.classList.add('open'); answer.style.maxHeight = answer.scrollHeight + 'px'; } else { answer.classList.remove('open'); answer.style.maxHeight = '0px'; } } document.querySelectorAll('.faq-question').forEach(btn => { btn.addEventListener('click', () => { const expanded = btn.getAttribute('aria-expanded') === 'true'; const answer = btn.nextElementSibling; document.querySelectorAll('.faq-question').forEach(b => { b.setAttribute('aria-expanded', 'false'); setFaqAnswerHeight(b.nextElementSibling, false); }); if (!expanded) { btn.setAttribute('aria-expanded', 'true'); setFaqAnswerHeight(answer, true); } }); }); window.addEventListener('resize', () => { document.querySelectorAll('.faq-question[aria-expanded="true"]').forEach(btn => { const answer = btn.nextElementSibling; if (answer.classList.contains('open')) { answer.style.maxHeight = answer.scrollHeight + 'px'; } }); }); // ---- Яндекс.Карта: проверка загрузки iframe ---- const ymapFrame = document.getElementById('ymap-frame'); const mapFallback = document.getElementById('map-fallback'); // Подставляем реальный iframe с картой через Yandex Maps short link // Если embed не поддерживается — показываем кнопку if (ymapFrame) { mapFallback.style.display = 'none'; } // ---- Галерея: лайтбокс ---- const lightbox = document.getElementById('lightbox'); const lightboxImg = lightbox?.querySelector('.lightbox-img'); const galleryItems = Array.from(document.querySelectorAll('.gallery-item')); let lightboxIndex = 0; function openLightbox(index) { if (!lightbox || !lightboxImg || !galleryItems.length) return; lightboxIndex = index; const img = galleryItems[lightboxIndex].querySelector('img'); lightboxImg.src = img.src; lightboxImg.alt = img.alt; lightbox.hidden = false; lightbox.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; } function closeLightbox() { if (!lightbox) return; lightbox.hidden = true; lightbox.setAttribute('aria-hidden', 'true'); lightboxImg.src = ''; document.body.style.overflow = ''; } function showLightboxStep(delta) { if (!galleryItems.length) return; lightboxIndex = (lightboxIndex + delta + galleryItems.length) % galleryItems.length; const img = galleryItems[lightboxIndex].querySelector('img'); lightboxImg.src = img.src; lightboxImg.alt = img.alt; } galleryItems.forEach((item, index) => { item.addEventListener('click', () => openLightbox(index)); }); lightbox?.querySelector('.lightbox-close')?.addEventListener('click', closeLightbox); lightbox?.querySelector('.lightbox-prev')?.addEventListener('click', () => showLightboxStep(-1)); lightbox?.querySelector('.lightbox-next')?.addEventListener('click', () => showLightboxStep(1)); lightbox?.addEventListener('click', (e) => { if (e.target === lightbox) closeLightbox(); }); document.addEventListener('keydown', (e) => { if (lightbox?.hidden) return; if (e.key === 'Escape') closeLightbox(); if (e.key === 'ArrowLeft') showLightboxStep(-1); if (e.key === 'ArrowRight') showLightboxStep(1); }); // ---- Плавное появление секций при прокрутке ---- const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); observer.unobserve(entry.target); } }); }, { threshold: 0.1 } ); document.querySelectorAll('.timeline-card, .faq-item, .gallery-item, .coordinator-card, .stat-card, .stats-two-col').forEach(el => { el.style.opacity = '0'; el.style.transform = 'translateY(24px)'; el.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; observer.observe(el); }); document.addEventListener('animationend', () => {}, { once: true }); // Добавляем класс visible через CSS const style = document.createElement('style'); style.textContent = '.visible { opacity: 1 !important; transform: none !important; }'; document.head.appendChild(style);