/* Shared data + orbital-state hook for the routes widget. window.ORBIT_ROUTES обычно подставляется сервером из БД (см. main.py /index). Массив ниже — фолбэк на случай пустой БД / ошибки рендера. */ window.ORBIT_ROUTES = window.ORBIT_ROUTES && window.ORBIT_ROUTES.length ? window.ORBIT_ROUTES : [ { id: "krasnaya", idx: "01", name: "Красная линия", kind: "Исторический центр", color: "#bd3b34", duration: "1,5 – 2 ч", href: "/routes/krasnaya", description: "Главная парадная линия Читы. От Казанского собора по улице Ленина — мимо купеческих особняков и доходных домов к зданию Драмтеатра.", hero: "/static/images/hero-cathedral.jpg", stops: [ { name: "Казанский собор", img: "/static/images/hero-cathedral.jpg" }, { name: "Дворец Шумовых", img: "/static/images/shumovih.jpg" }, { name: "Улица Амурская", img: "/static/images/amurskaya.jpg" }, { name: "Драматический театр", img: "/static/images/Theatre.jpg" }, { name: "Доходный дом, 1907", img: "/static/images/dohodny.jpg" }, ], }, { id: "decembrists", idx: "02", name: "Квартал Декабристов", kind: "История и ссылка", color: "#c98a2e", duration: "1 – 1,5 ч", href: "/routes/decembrists", description: "Линия памяти декабристов. Деревянная Михайло-Архангельская церковь и кварталы вокруг неё — XIX век в дереве и тишине.", hero: "/static/images/decabristov.jpg", stops: [ { name: "Церковь Декабристов", img: "/static/images/decabristov.jpg" }, { name: "Михайло-Архангельская", img: "/static/images/missionerskoe.jpg" }, { name: "Доходный дом купца", img: "/static/images/dohodny.jpg" }, ], }, { id: "zelenaya", idx: "03", name: "Зелёная линия", kind: "Храмы и сады", color: "#3f8f5b", duration: "1,5 ч", href: "/routes/zelenaya", description: "Спокойная линия для неспешной прогулки: храмы, скверы, тенистые улицы и виды на сопки, что весной розовеют от багульника.", hero: "/static/images/green-church.jpg", stops: [ { name: "Воскресенская церковь", img: "/static/images/green-church.jpg" }, { name: "Михайло-Архангельская", img: "/static/images/missionerskoe.jpg" }, { name: "Улица Амурская", img: "/static/images/amurskaya.jpg" }, ], }, { id: "baguilovaya", idx: "04", name: "Багуловая линия", kind: "Виды и природа", color: "#b357c4", duration: "2 ч", href: "/routes/baguilovaya", description: "Видовая линия — подъём к панорамам города в обрамлении сопок, которые в мае заливает розовый багульник.", hero: "/static/images/pink-baroque.jpg", stops: [ { name: "Казанский собор", img: "/static/images/hero-cathedral.jpg" }, { name: "Особняк на Амурской", img: "/static/images/pink-baroque.jpg" }, { name: "Воскресенская церковь", img: "/static/images/green-church.jpg" }, ], }, ]; /* useOrbit — shared rotation + selection state. targetRef === null → свободное вращение; число → плавно доезжаем до этого угла («перестройка»), затем замираем на нём (snap + hold). */ window.useOrbit = function useOrbit({ speed = 0.18 } = {}) { const { useState, useEffect, useRef } = React; const [angle, setAngle] = useState(0); const [activeId, setActiveId] = useState(null); const [hoverId, setHoverId] = useState(null); const targetRef = useRef(null); const rafRef = useRef(0); const lastT = useRef(performance.now()); const setTarget = (v) => { targetRef.current = (v == null ? null : ((v % 360) + 360) % 360); }; useEffect(() => { let alive = true; const tick = (t) => { if (!alive) return; const dt = t - lastT.current; lastT.current = t; setAngle((a) => { const target = targetRef.current; if (target == null) { return (a + (dt / 16) * speed) % 360; } // едем по кратчайшему пути к target, затем фиксируемся const diff = ((target - a + 540) % 360) - 180; if (Math.abs(diff) < 0.04) return target; return a + diff * Math.min(1, (dt / 16) * 0.06); }); rafRef.current = requestAnimationFrame(tick); }; rafRef.current = requestAnimationFrame(tick); return () => { alive = false; cancelAnimationFrame(rafRef.current); }; }, [speed]); return { angle, activeId, setActiveId, hoverId, setHoverId, setTarget }; }; /* polar — returns {x,y} for given angle (deg) and radius */ window.polar = function polar(angleDeg, radius) { const r = (angleDeg * Math.PI) / 180; return { x: Math.cos(r) * radius, y: Math.sin(r) * radius }; };