'use strict'; const { user, isTeacher, isAdmin } = LS.initPage(); window._simQuizAllowed = true; // default; overridden after permission fetch for students LS.showBoardIfAllowed(); /* ════════════════════════════════ SIM CATALOGUE (defined after P_* consts below) ════════════════════════════════ */ let _catFilter = 'all'; var _disabledSimIds = new Set(); let _simModuleDisabled = false; function filterSims(cat, btn) { _catFilter = cat; document.querySelectorAll('.lab-filter').forEach(b => b.classList.remove('active')); btn.classList.add('active'); renderSims(); } function renderSims() { const base = _catFilter === 'all' ? SIMS : SIMS.filter(s => s.cat === _catFilter); const list = base.filter(s => !s.id || !_disabledSimIds.has(s.id)); document.getElementById('sim-grid').innerHTML = list.map(s => `
${s.preview}
${s.cat === 'math' ? '∑ Математика' : s.cat === 'chem' ? ' Химия' : s.cat === 'bio' ? ' Биология' : s.cat === 'game' ? ' Игры' : LS.icon('zap',14) + ' Физика'}
${s.title}
${s.desc}
${!s.id ? '
Скоро
' : ''}
`).join(''); if (window.lucide) lucide.createIcons(); } /* ════════════════════════════════ CARD PREVIEW SVGs ════════════════════════════════ */ function _grid(fg='rgba(255,255,255,0.06)') { return ` `; } function _axes() { return ` `; } function _svg(body) { return ` ${body}`; } /* 1 — Graph */ const P_GRAPH = _svg(`${_grid()}${_axes()} `); /* 2 — Transform: three shifted/scaled sines */ const P_TRANSFORM = _svg(`${_grid()}${_axes()} `); /* 3 — Triangle geometry */ const P_TRIANGLE = _svg(`${_grid('rgba(255,255,255,0.04)')} `); /* 4 — Inscribed/circumscribed circles */ const P_CIRCLES = _svg(`${_grid('rgba(255,255,255,0.04)')} `); /* 5 — Quadratic roots: parabola crossing x-axis */ const P_QUADRATIC = _svg(`${_grid()}${_axes()} D = b²− 4ac`); /* 6 — 3D geometry: isometric cube */ const P_3D = _svg(`${_grid('rgba(255,255,255,0.04)')} V = a³`); /* 7 — Probability: histogram bars */ const P_PROB = _svg(`${_grid()} `); /* 8 — Normal distribution: bell curve */ const P_NORMAL = _svg(`${_grid()} μ = 0, σ = 1`); /* 8b — Trig circle */ const P_TRIGCIRCLE = _svg(`${_grid('rgba(255,255,255,0.04)')} sin · cos · tg · ctg`); /* 9 — Projectile motion */ const P_PROJECTILE = _svg(`${_grid('rgba(255,255,255,0.04)')} x = v₀cos(α)·t`); /* 10 — Pendulum */ const P_PENDULUM = _svg(`${_grid('rgba(255,255,255,0.04)')} T = 2π√(l/g)`); /* 11 — Collision */ const P_COLLISION = _svg(`${_grid('rgba(255,255,255,0.04)')} m₁ m₂ `); /* 13 — Electric circuit */ const P_CIRCUIT = _svg(`${_grid('rgba(255,255,255,0.04)')} R₁ R₂ I = U/R`); /* 14 — Magnetic field */ const P_MAGNETIC = _svg(` ${_grid('rgba(155,93,229,0.06)')} B = μ₀I / 2πr`); /* 14 — Electric field lines */ const P_FIELD = _svg(`${_grid('rgba(255,255,255,0.04)')} + `); /* 15 — Thin lens */ const P_LENS = _svg(`${_grid('rgba(255,255,255,0.04)')} `); /* 16 — Refraction */ const P_REFRACTION = _svg(` α β n₁sinα = n₂sinβ`); /* 17 — Mirrors */ const P_MIRROR = _svg(`${_grid('rgba(255,255,255,0.04)')} F `); /* 18 — Isoprocesses */ const P_ISOPROCESS = _svg(`${_grid('rgba(255,255,255,0.04)')} 2 1 V P`); /* ── Chemistry / Molecular Physics previews ── */ const P_GAS = _svg(` ${[ [40,30,'#4CC9F0'],[70,80,'#7BF5A4'],[110,25,'#EF476F'],[150,60,'#FFD166'],[190,30,'#4CC9F0'], [220,90,'#EF476F'],[55,110,'#7BF5A4'],[95,65,'#4CC9F0'],[130,110,'#EF476F'],[170,40,'#FFD166'], [210,115,'#4CC9F0'],[240,55,'#7BF5A4'],[30,70,'#FFD166'],[80,120,'#EF476F'],[165,95,'#4CC9F0'] ].map(([x,y,c])=>``).join('')} PV=nRT`); /* ── Законы Ньютона ── */ const P_NEWTON = _svg(` F m₂ a = F/m · III законы Ньютона`); /* ── Песочница сил ── */ const P_SANDBOX = _svg(` ${_grid('rgba(255,255,255,0.03)')} 5кг 8кг F₁ F₂ Песочница сил · F = ma`); const P_HYDRO = _svg(` ${_grid('rgba(255,255,255,0.03)')} P = ρgh F_A Δh Архимед · Паскаль · капиллярность`); /* ── coming soon chem previews (simple) ── */ const P_KINETICS = _svg(` ${_grid()} [C] продукт [A] реагент`); const P_EQUILIBRIUM = _svg(` A + B ⇌ C + D A,B K C,D`); const P_ELECTROLYSIS = _svg(` ${[55,58,61,64,67,70].map(x=>``).join('')} ${[210,214,218,222,226].map(x=>``).join('')} +`); const P_BOHR = _svg(` `); const P_ORBITALS = _svg(` `); const P_PH = _svg(` ${_grid()} pH V`); const P_CHEMSANDBOX = _svg(` ${_grid()} A + B C + D `); const P_STOICHIOMETRY = _svg(` ${_grid()} Zn 2HCl ZnCl₂ H₂ ● лимит`); /* Periodic Table — 6×4 coloured cell grid */ const P_PERIODIC = _svg(` ${(function(){ const cols=18,rows=4,pad=6,w=(270-pad*2)/cols,h=(140-pad*2)/rows; const colors=['#EF476F','#FF6B35','#FFD166','#7BF5A4','#C77DFF','#A8DADC', '#7B8EF7','#06D6E0','#9B5DE5','#F15BB5','#EF476F','#FF6B35', '#06D6E0','#7B8EF7','#FFD166','#C77DFF','#A8DADC','#7BF5A4']; let s=''; for(let r=0;r=2&&c<=15)||(r===1&&c>=2&&c<=11); if(!skip) s+=``; } return s; })()} 118 элементов`); const P_CRYSTAL = _svg(` ${[ [80,40],[135,40],[190,40], [55,75],[110,75],[165,75],[220,75], [80,110],[135,110],[190,110] ].map(([x,y],i)=>``).join('')} `); const P_CELLDIVISION = _svg(` Метафаза · митоз`); const P_PHOTOSYNTHESIS = _svg(` H₂O CO₂ ATP G3P Световые реакции · цикл Кальвина`); const P_ANGRYBIRDS = _svg(` Физика полёта · импульс · разрушение`); const P_WAVES = _svg(`${_grid()} v = \u03bbf \u00b7 y = A sin(\u03c9t \u2212 kx) \u00b7 \u0441\u0442\u043e\u044f\u0447\u0438\u0435 \u0432\u043e\u043b\u043d\u044b`); /* Radioactive decay preview */ const P_RADIOACTIVE = _svg(`${_grid()} N(t) = N₀·e⁻λt · T½ · цепочки распада`); /* Heat Engines preview */ const P_HEATENGINE = _svg(`${_grid('rgba(255,255,255,0.04)')} A B C V P η = 1 − Tc/Th`); /* Geometry (planimetry) preview */ const P_GEOMETRY = _svg(`${_grid('rgba(255,255,255,0.04)')} A B C`); /* Race sim preview — two objects on a track, x(t) lines */ const P_RACE = _svg(`${_grid('rgba(255,255,255,0.05)')} встреча x = x₀ + v₀t + at²/2`); /* Logic Circuits preview */ const P_LOGIC = _svg(`${_grid('rgba(255,255,255,0.04)')} AND XOR AND S C S = A⊕B · C = A∧B · Таблица истинности`); /* Qualitative Analysis preview */ const P_QUALANALYSIS = _svg(` Cl Fe(III) SO4 AgNO3 AgCl / Fe(SCN) / BaSO4`); /* Organic Chemistry preview — benzene ring + OH group */ const P_ORGANIC = _svg(` ${_grid('rgba(255,255,255,0.03)')} C C C C C C O H C Конструктор · Ряды · Качественные реакции`); /* Solutions preview */ const P_SOLUTIONS = _svg(` 20% ω% C-M ν моль ω = m₀/m​ · 100% Калькулятор · Разбавление · Смешивание · S(T)`); const SIMS = [ /* ── Математика ── */ { id: 'graph', cat: 'math', title: 'График функции', desc: 'Строй графики функций y = f(x) с параметрами, зумом и курсором координат.', preview: P_GRAPH }, { id: 'graphtransform', cat: 'math', title: 'Трансформации графиков', desc: 'Наблюдай, как сдвиги, растяжения и отражения меняют вид функции y = a·f(kx+b)+c.', preview: P_TRANSFORM }, { id: 'geometry', cat: 'math', title: 'Планиметрия', desc: 'Интерактивная среда построений: точки, отрезки, прямые, окружности, многоугольники. Полноценный чертёж с привязкой и измерениями.', preview: P_GEOMETRY }, { id: 'triangle', cat: 'math', title: 'Геометрия треугольника', desc: 'Интерактивный треугольник: медианы, высоты, биссектрисы, вписанная и описанная окружности.', preview: P_TRIANGLE }, { id: 'quadratic', cat: 'math', title: 'Корни квадратного уравнения', desc: 'Задай a, b, c ползунками — смотри дискриминант и корни анимированно на числовой оси.', preview: P_QUADRATIC }, { id: 'stereo', cat: 'math', title: 'Стереометрия 3D', desc: 'Вращаемые объёмные фигуры: куб, пирамида, цилиндр, конус с формулами объёма и площади. Сечения, развёртка, вписанные/описанные сферы.', preview: P_3D }, { id: 'probability', cat: 'math', title: 'Теория вероятностей', desc: 'Подброс монеты/кубика N раз — гистограмма частот и закон больших чисел в действии.', preview: P_PROB }, { id: 'trigcircle', cat: 'math', title: 'Тригонометрическая окружность', desc: 'Единичная окружность с sin, cos, tg, ctg. Перетаскивай точку — все функции обновляются мгновенно. График синхронизирован.', preview: P_TRIGCIRCLE }, { id: 'normaldist', cat: 'math', title: 'Нормальное распределение', desc: 'Двигай μ и σ ползунками — колокол Гаусса и площадь под кривой обновляются мгновенно.', preview: P_NORMAL }, /* ── Физика ── */ { id: 'projectile', cat: 'phys', title: 'Бросок тела', desc: 'Задай начальную скорость и угол — симулируй траекторию, дальность и высоту полёта.', preview: P_PROJECTILE }, { id: 'pendulum', cat: 'phys', title: 'Маятник', desc: 'Регулируй длину и угол отклонения — изучай период колебаний и затухание.', preview: P_PENDULUM }, { id: 'collision', cat: 'phys', title: 'Столкновение шаров', desc: 'Упругий и неупругий удар двух тел: законы сохранения импульса и энергии.', preview: P_COLLISION }, { id: 'emfield', cat: 'phys', title: 'Электромагнитные поля', desc: 'Электрическое и магнитное поля в одной симуляции: заряды, токи, силовые линии, эквипотенциали, частица Лоренца.', preview: P_MAGNETIC }, { id: 'circuit', cat: 'phys', title: 'Электрические цепи', desc: 'Конструктор цепей из резисторов и конденсаторов. Законы Ома и Кирхгофа наглядно.', preview: P_CIRCUIT }, { id: 'hydrostatics', cat: 'phys', title: 'Гидростатика', desc: 'Давление жидкости P=ρgh, закон Архимеда, сообщающиеся сосуды, поверхностное натяжение и капиллярность.', preview: P_HYDRO }, { id: 'dynamics', cat: 'phys', title: 'Динамика', desc: 'Законы Ньютона, песочница сил, наклонная плоскость — всё в одном интерактивном модуле.', preview: P_SANDBOX }, { id: 'opticsbench', cat: 'phys', title: 'Оптическая скамья', desc: 'Линза, зеркала и преломление в одной симуляции: формула линзы, зеркальное отражение, закон Снеллиуса, ПВО, дисперсия.', preview: P_LENS }, { id: 'isoprocess', cat: 'phys', title: 'Изопроцессы', desc: 'PV-диаграмма для четырёх изопроцессов идеального газа. Расчёт работы, теплоты и внутренней энергии.', preview: P_ISOPROCESS }, { id: 'waves', cat: 'phys', title: 'Волны и звук', desc: 'Поперечные и продольные волны, суперпозиция, стоячие волны. Частота, амплитуда, фаза, гармоники.', preview: P_WAVES }, { id: 'radioactive', cat: 'phys', title: 'Радиоактивный распад', desc: 'Период полураспада, цепочки распадов, активность. Визуализация ядер + кривая N(t). Радиоуглеродное датирование.', preview: P_RADIOACTIVE }, { id: 'race', cat: 'phys', title: 'Гонка с задачами', desc: 'Кинематика 1D: встреча, догон, кто первый. Реши задачу — проверь анимацией и графиком x(t).', preview: P_RACE }, { id: 'heatengine', cat: 'phys', title: 'Тепловые двигатели', desc: 'Циклы Карно, Отто, Дизеля, Брайтона. PV-диаграмма, поршень, КПД.', preview: P_HEATENGINE }, { id: 'logic', cat: 'phys', title: 'Логические схемы', desc: 'Конструктор цифровых схем: И/ИЛИ/НЕ/XOR, триггеры, сумматоры. Авто-таблица истинности.', preview: P_LOGIC }, /* ── Химия / Молекулярная физика ── */ { id: 'molphys', cat: 'chem', title: 'Молекулярная физика', desc: 'Идеальный газ, броуновское движение, агрегатные состояния и диффузия — всё в одном модуле.', preview: P_GAS }, { id: 'chemistry', cat: 'chem', title: 'Химические реакции', desc: 'Кинетика реакций, металл + кислота в колбе, ОВР с переносом электронов, ионный обмен — всё в одном модуле.', preview: P_KINETICS }, { id: 'equilibrium', cat: 'chem', title: 'Химическое равновесие', desc: 'Прямая и обратная реакция, принцип Ле Шателье: изменяй T, P, концентрацию и наблюдай сдвиг.', preview: P_EQUILIBRIUM }, { id: 'electrolysis', cat: 'chem', title: 'Электролиз', desc: 'Катод и анод в растворе электролита: движение ионов, выделение газа, закон Фарадея.', preview: P_ELECTROLYSIS }, /* ── Скоро: Атомная структура ── */ { id: 'bohratom', cat: 'chem', title: 'Атом Бора', desc: 'Электроны на орбитах, квантование энергии, эмиссия и поглощение фотонов при переходах.', preview: P_BOHR }, { id: 'orbitals', cat: 'chem', title: 'Молекулярные орбитали', desc: 'H₂, H₂O — ковалентная связь, перекрывание орбиталей, 3D-визуализация электронных облаков.', preview: P_ORBITALS }, /* ── Скоро: Визуальная химия ── */ { id: 'titration', cat: 'chem', title: 'pH и кривая титрования', desc: 'Добавляй кислоту или щёлочь — наблюдай изменение pH, цвет раствора и кривую нейтрализации.', preview: P_PH }, { id: 'chemsandbox', cat: 'chem', title: 'Химическая песочница', desc: 'Смешивай реагенты, наблюдай реакции: осадки, газы, изменение цвета. Свободное экспериментирование.', preview: P_CHEMSANDBOX }, { id: 'stoichiometry', cat: 'chem', title: 'Стехиометрия', desc: 'Расчёты по уравнениям: масса, моль, объём. Лимитирующий реагент, выход. 10 реакций.', preview: P_STOICHIOMETRY }, { id: 'crystal', cat: 'chem', title: 'Кристаллическая решётка', desc: 'NaCl, алмаз, металл — интерактивная 3D-решётка, типы связей, вращение структуры.', preview: P_CRYSTAL }, { id: 'qualanalysis', cat: 'chem', title: 'Качественный анализ', desc: 'Определяй катионы и анионы качественными реакциями: осадки, газы, пламя. Два режима: guided и свободный эксперимент.', preview: P_QUALANALYSIS }, { id: 'periodic', cat: 'chem', title: 'Периодическая таблица', desc: '118 элементов: подсветка по типу/блоку, карточка элемента, боровские оболочки, графики свойств.', preview: P_PERIODIC }, { id: 'organic', cat: 'chem', title: 'Органическая химия', desc: 'Конструктор молекул с проверкой валентности, гомологические ряды с таблицей свойств, качественные реакции (бромная вода, KMnO₄, зеркало Толленса, Cu(OH)₂, FeCl₃, Na).', preview: P_ORGANIC }, { id: 'solutions', cat: 'chem', title: 'Растворы', desc: 'Калькулятор раствора: ω, ν, C_M, плотность. Разбавление и смешивание с визуализацией. Кривые растворимости S(T) для 8 веществ + задача на перекристаллизацию.', preview: P_SOLUTIONS }, /* ── Биология ── */ { id: 'celldivision', cat: 'bio', title: 'Деление клетки', desc: 'Митоз и мейоз: анимированные фазы, хромосомы, веретено деления, ядерная оболочка.', preview: P_CELLDIVISION }, { id: 'photosynthesis', cat: 'bio', title: 'Фотосинтез и дыхание', desc: 'Световые реакции в тилакоидах, цикл Кальвина, митохондриальное дыхание — молекулярная анимация.', preview: P_PHOTOSYNTHESIS }, /* ── Игры ── */ { id: 'angrybirds', cat: 'game', title: 'Angry Birds Physics', desc: 'Запускай птиц из рогатки, разрушай блоки, побеждай свиней. Реальная физика: гравитация, ветер, импульс. 6 уровней.', preview: P_ANGRYBIRDS }, ]; var _theoryOpen = false; function toggleTheory() { _theoryOpen = !_theoryOpen; document.getElementById('theory-panel').classList.toggle('open', _theoryOpen); const btn = document.getElementById('theory-toggle'); btn.style.background = _theoryOpen ? 'rgba(155,93,229,0.15)' : ''; btn.style.borderColor = _theoryOpen ? 'var(--violet)' : ''; btn.style.color = _theoryOpen ? 'var(--violet)' : ''; } function loadTheory(simId) { const t = THEORY[simId]; const el = document.getElementById('theory-content'); if (!t) { el.innerHTML = '
Теория для этой симуляции пока не добавлена
'; return; } let html = `
${LS.icon('book-open',16)} ${t.title}
`; for (const s of t.sections) { html += '
'; if (s.head) html += `
${s.head}
`; if (s.formula) html += `
`; if (s.text) html += `
${s.text}
`; if (s.vars) html += `
${s.vars.map(([v,d]) => `
${v} — ${d}
`).join('')}
`; html += '
'; } el.innerHTML = html; // render KaTeX formulas el.querySelectorAll('.tp-formula[data-formula]').forEach(div => { try { katex.render(div.dataset.formula, div, { displayMode: true, throwOnError: false }); } catch(e) { div.textContent = div.dataset.formula; } }); } /* ── embed mode + auto-open from ?sim= ── */ const _qp = new URLSearchParams(location.search); var _embedMode = _qp.get('embed') === '1'; var _autoSim = _qp.get('sim'); /* ── Sim state relay (embed mode only) ──────────────────────────────── */ // Map simId → { getState, applyState } registered by openSim handlers const _simStateRegistry = {}; function _registerSimState(simId, getState, applyState) { _simStateRegistry[simId] = { getState, applyState }; } let _lastEmittedState = null; let _stateEmitInterval = null; function _startStateEmit(simId) { if (_stateEmitInterval) clearInterval(_stateEmitInterval); _lastEmittedState = null; _stateEmitInterval = setInterval(() => { const reg = _simStateRegistry[simId]; if (!reg) return; try { const state = reg.getState(); const json = JSON.stringify(state); if (json === _lastEmittedState) return; _lastEmittedState = json; window.parent.postMessage({ type: 'sim_state', simId, state }, '*'); } catch {} }, 400); } function _stopStateEmit() { if (_stateEmitInterval) { clearInterval(_stateEmitInterval); _stateEmitInterval = null; } _lastEmittedState = null; } // Receive apply_sim_state from parent (students) window.addEventListener('message', e => { if (!_embedMode) return; const d = e.data; if (!d || d.type !== 'apply_sim_state') return; const reg = _simStateRegistry[_autoSim]; if (!reg) return; try { reg.applyState(d.state); _lastEmittedState = JSON.stringify(d.state); // suppress echo } catch {} }); if (_embedMode) { document.querySelector('.sidebar').style.display = 'none'; document.querySelector('.sb-content').style.marginLeft = '0'; document.querySelector('.app-layout').classList.add('embed-mode'); document.getElementById('lab-home').style.display = 'none'; document.getElementById('theory-toggle').style.display = 'none'; if (_autoSim) { document.getElementById('lab-sim').classList.add('open'); document.querySelector('.sim-topbar').style.display = 'none'; // defer until all external scripts are loaded window.addEventListener('load', () => openSim(_autoSim)); } } else { /* init — fetch sim settings + permissions in parallel, then render */ const _permFetch = (!isTeacher && !isAdmin) ? LS.api('/api/permissions/me').catch(() => null) : Promise.resolve(null); Promise.all([ LS.api('/api/settings/sims').catch(() => ({})), _permFetch, ]).then(([cfg, permData]) => { _simModuleDisabled = cfg.module_disabled || false; _disabledSimIds = new Set(cfg.disabled_ids || []); // check simulations.access for students if (!isTeacher && !isAdmin && permData) { const p = permData.permissions?.find(p => p.key === 'simulations.access'); if (p && p.effective === false) { document.getElementById('sim-grid').innerHTML = `
Доступ к симуляциям закрыт
Администратор ограничил доступ к лаборатории
`; return; } // store quiz permission for later use const qp = permData.permissions?.find(p => p.key === 'simulations.quiz'); window._simQuizAllowed = !qp || qp.effective !== false; } else { window._simQuizAllowed = true; } if (_simModuleDisabled) { document.getElementById('sim-grid').innerHTML = `
Модуль симуляций отключён
Администратор временно отключил лабораторию
`; } else { renderSims(); if (_autoSim) openSim(_autoSim); // hash-router: activate sim from URL fragment after catalogue renders else _activateFromHash(); } }); lucide.createIcons(); LS.notif.init(); } /* ─── Hash router for sim deep-links ───────────────────────────────────── URL pattern: /lab#sim/ matches SIMS[i].id (e.g. 'projectile', 'graph', 'chemsandbox'). F5 restores sim. Browser back/forward switches between sims. Click on sim-card updates URL via wrapped openSim. ──────────────────────────────────────────────────────────────────────── */ // Build valid-id set from SIMS catalogue (filters out "coming soon" entries) const _SIM_HASH_MAP = {}; SIMS.forEach(function(s) { if (s.id) { _SIM_HASH_MAP[s.id] = s.id; } }); // backward-compat aliases: old URLs redirect to unified emfield sim _SIM_HASH_MAP['magnetic'] = 'magnetic'; _SIM_HASH_MAP['coulomb'] = 'coulomb'; // backward-compat aliases: old optics sims redirect to opticsbench _SIM_HASH_MAP['thinlens'] = 'opticsbench'; _SIM_HASH_MAP['mirrors'] = 'opticsbench'; _SIM_HASH_MAP['refraction'] = 'opticsbench'; var _routerNavigating = false; function _activateFromHash() { var m = (location.hash || '').match(/^#sim\/([\w-]+)/); if (!m) return false; var simName = m[1]; if (!_SIM_HASH_MAP[simName]) { // eslint-disable-next-line no-console window.console && window.console.warn('lab-router: unknown sim', simName); return false; } openSim(simName); return true; } // Intercept openSim to push URL hash on user-initiated navigation var _origOpenSim = openSim; openSim = function(id) { _origOpenSim(id); if (!_routerNavigating && !_embedMode) { var baseId = id.includes(':') ? id.split(':')[0] : id; if (_SIM_HASH_MAP[baseId]) { _routerNavigating = true; location.hash = '#sim/' + baseId; // use setTimeout so hashchange fires after flag is set setTimeout(function() { _routerNavigating = false; }, 0); } } }; /* ─── Sim Fade Transition + View Transitions API ───────────────────────── Wraps openSim with a fade-out (150ms) → swap → fade-in (200ms) sequence. If document.startViewTransition is available it is used for GPU-composited cross-fade; otherwise the manual .sim-fading CSS class is toggled. The hash-router wrap above runs synchronously during the transition so URL updates are not delayed. ──────────────────────────────────────────────────────────────────────── */ var _hashRouterOpenSim = openSim; // reference after hash-router wrap openSim = function(id) { var labSim = document.getElementById('lab-sim'); if (!labSim) { _hashRouterOpenSim(id); return; } function _doSwitch() { labSim.classList.add('sim-fading'); setTimeout(function() { _hashRouterOpenSim(id); labSim.classList.remove('sim-fading'); }, 150); } if (typeof document.startViewTransition === 'function' && !_embedMode) { document.startViewTransition(function() { _hashRouterOpenSim(id); }); } else { _doSwitch(); } }; // Intercept closeSim to clear hash when returning to home grid var _origCloseSim = closeSim; closeSim = function() { _origCloseSim(); if (!_embedMode) { _routerNavigating = true; history.pushState(null, '', location.pathname + location.search); setTimeout(function() { _routerNavigating = false; }, 0); } }; // Browser back/forward navigation window.addEventListener('hashchange', function() { if (_routerNavigating) return; var hasHash = _activateFromHash(); if (!hasHash && document.getElementById('lab-sim').classList.contains('open')) { _origCloseSim(); } });