diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index fcb3ecb..27a3648 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -77,12 +77,14 @@

Ваш SQL запрос:
- +
-
-

Пример данных:

-
+
+

Пример данных:

+
+
+ diff --git a/src/main/resources/static/script.js b/src/main/resources/static/script.js index 349e3f1..607ca3f 100644 --- a/src/main/resources/static/script.js +++ b/src/main/resources/static/script.js @@ -443,6 +443,7 @@ function showResult(message, isSuccess, resultData = {}) { const nextBtn = document.getElementById('next-task-btn'); if (nextBtn) { nextBtn.addEventListener('click', () => { + resetVisualization(); showTask(currentTaskIndex + 1); container.style.display = 'none'; }); @@ -527,6 +528,20 @@ function setupQueryInput() { const cursor = document.createElement('span'); cursor.className = 'cursor'; queryInput.parentNode.insertBefore(cursor, queryInput.nextSibling); + + const submitBtn = document.getElementById('query-submit'); + submitBtn.addEventListener('click', async () => { + const query = queryInput.value.trim(); + if (!query) return; + // Космическая визуализация + const visContainer = document.getElementById('visualizationContainer'); + visContainer.classList.add('active'); + visualizeQuery(query); + // Ждем завершения анимации (1.3 сек), только потом проверяем запрос и показываем результат + await new Promise(res => setTimeout(res, 1300)); + await checkQuery(); + visContainer.classList.remove('active'); + }); } function setupEventListeners() { @@ -548,8 +563,6 @@ function setupEventListeners() { } }); - document.getElementById('query-submit').addEventListener('click', checkQuery); - document.getElementById('query-input').addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'Enter') { checkQuery(); @@ -740,6 +753,129 @@ function createJoinEffect() { } } +// Космическая визуализация запроса +function visualizeQuery(sqlQuery) { + const container = document.getElementById('visualizationContainer'); + container.innerHTML = ''; + + // Смайлик-пользователь 👨‍💻 + подпись + const userBlock = document.createElement('div'); + userBlock.className = 'cosmo-user'; + userBlock.innerHTML = '👨‍💻
Вы
'; + container.appendChild(userBlock); + + // Найти блок примера данных (БД) + const dbSidebar = document.querySelector('.data-example-sidebar'); + if (!dbSidebar) return; + + // Получить координаты блока БД относительно visualizationContainer + const visRect = container.getBoundingClientRect(); + const dbRect = dbSidebar.getBoundingClientRect(); + const offsetLeft = dbRect.left - visRect.left; + const offsetTop = dbRect.top - visRect.top; + const dbWidth = dbRect.width; + const dbHeight = dbRect.height; + + // Динамически создать подпись <сервер> над примером данных (появляется только на время анимации) + const serverLabel = document.createElement('div'); + serverLabel.className = 'server-label cosmo-server-label-anim'; + serverLabel.textContent = ''; + serverLabel.style.left = (offsetLeft + dbWidth/2) + 'px'; + serverLabel.style.top = (offsetTop - 38) + 'px'; + serverLabel.style.opacity = '0'; + serverLabel.style.position = 'absolute'; + serverLabel.style.transform = 'translateX(-50%)'; + container.appendChild(serverLabel); + + // Неоновое облако вокруг блока БД + const dbGlow = document.createElement('div'); + dbGlow.className = 'cosmo-db-glow'; + dbGlow.style.left = offsetLeft + 'px'; + dbGlow.style.top = offsetTop + 'px'; + dbGlow.style.width = dbWidth + 'px'; + dbGlow.style.height = dbHeight + 'px'; + dbGlow.style.opacity = '0'; + container.appendChild(dbGlow); + + // Тонкая кривая стрелка (SVG), не заходит на пример данных + const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + arrow.setAttribute('class', 'cosmo-arrow-curve'); + arrow.setAttribute('width', Math.max(180, offsetLeft + dbWidth/2)); + arrow.setAttribute('height', Math.max(100, offsetTop - 10)); + arrow.style.position = 'absolute'; + arrow.style.left = '0px'; + arrow.style.top = '0px'; + // Старт: смайлик, конец — чуть выше блока сервера + const startX = 55, startY = 65; + const endX = offsetLeft + dbWidth/2, endY = offsetTop - 18; + const c1x = startX + 60, c1y = startY - 40; + const c2x = endX - 60, c2y = endY + 40; + arrow.innerHTML = ` + + + + + + + + + + `; + container.appendChild(arrow); + + // Анимация стрелки, кружочка и появления <сервер> + setTimeout(() => { + const path = arrow.querySelector('.cosmo-arrow-path'); + if (path) path.classList.add('drawn'); + animateDotAlongPath(arrow, path, arrow.querySelector('#arrowDot'), arrow.querySelector('#arrowHead'), 1800); + setTimeout(() => { + dbGlow.classList.add('active'); + dbGlow.style.opacity = '1'; + serverLabel.style.opacity = '1'; + }, 900); + }, 200); +} + +// Анимация кружочка по SVG-пути (без изменений) +function animateDotAlongPath(svg, path, dot, arrowHead, duration) { + if (!path || !dot || !arrowHead) return; + const len = path.getTotalLength(); + let start = null; + function step(ts) { + if (!start) start = ts; + let progress = Math.min((ts - start) / duration, 1); + const point = path.getPointAtLength(len * progress); + dot.setAttribute('cx', point.x); + dot.setAttribute('cy', point.y); + // Поворот стрелки + if (arrowHead) { + const angle = progress < 0.99 + ? Math.atan2( + path.getPointAtLength(len * progress + 1).y - point.y, + path.getPointAtLength(len * progress + 1).x - point.x + ) * 180 / Math.PI + : Math.atan2( + point.y - path.getPointAtLength(len * progress - 1).y, + point.x - path.getPointAtLength(len * progress - 1).x + ) * 180 / Math.PI; + arrowHead.setAttribute('transform', `translate(${point.x},${point.y - 5}) rotate(${angle})`); + } + if (progress < 1) { + requestAnimationFrame(step); + } else { + dot.setAttribute('cx', point.x); + dot.setAttribute('cy', point.y); + } + } + requestAnimationFrame(step); +} + +// Сброс космической визуализации +function resetVisualization() { + const container = document.getElementById('visualizationContainer'); + if (container) container.innerHTML = ''; +} + // Инициализация приложения document.addEventListener('DOMContentLoaded', () => { initializeApp(); diff --git a/src/main/resources/static/styles.css b/src/main/resources/static/styles.css index 50b1a20..94a5063 100644 --- a/src/main/resources/static/styles.css +++ b/src/main/resources/static/styles.css @@ -1008,4 +1008,82 @@ h1 { text-align: center; animation: pulse 2s infinite; } +} + +/* --- КОСМИЧЕСКАЯ ВИЗУАЛИЗАЦИЯ V3 --- */ +#visualizationContainer { + position: relative; + min-height: 180px; + margin: 20px 0 10px 0; + z-index: 5; + pointer-events: none; +} +.cosmo-user { + position: absolute; + left: 10px; + top: 40px; + z-index: 10; + font-size: 2.8em; + filter: drop-shadow(0 0 10px #fffbe6); + display: flex; + flex-direction: column; + align-items: center; +} +.user-label { + font-size: 1em; + color: #bdbfff; + text-shadow: 0 0 8px #1a1a2e; + margin-top: 0.2em; + font-weight: bold; + letter-spacing: 0.5px; +} +.server-label { + color: #00f0ff; + font-size: 1.12em; + font-family: 'Roboto Mono', monospace; + text-shadow: 0 0 8px #00f0ff, 0 0 12px #a259ff; + letter-spacing: 1px; + z-index: 20; + pointer-events: none; + font-weight: bold; + white-space: nowrap; + opacity: 1; + transition: opacity 0.4s; +} +.cosmo-server-label-anim { + opacity: 0; + transition: opacity 0.4s; +} +.cosmo-server-label-anim[style*='opacity: 1'] { + opacity: 1; +} +.cosmo-arrow-curve { + z-index: 11; + pointer-events: none; +} +.cosmo-arrow-path { + stroke-dasharray: 800; + stroke-dashoffset: 800; + animation: drawCurve 1.8s cubic-bezier(.7,.1,.3,1.4) forwards; + filter: drop-shadow(0 0 6px #00f0ff) drop-shadow(0 0 12px #a259ff); + stroke-width: 2.1; +} +@keyframes drawCurve { + to { stroke-dashoffset: 0; } +} +.cosmo-db-glow { + position: absolute; + border-radius: 32px; + box-shadow: 0 0 32px 12px #a259ff88, 0 0 16px 6px #00f0ff77; + border: 2.5px solid #00f0ffcc; + pointer-events: none; + transition: box-shadow 0.6s, border-color 0.4s; + z-index: 12; + opacity: 0.5; + background: rgba(30,16,60,0.08); +} +.cosmo-db-glow.active { + box-shadow: 0 0 60px 24px #a259ffcc, 0 0 32px 12px #00f0ffcc; + border-color: #a259ff; + opacity: 1; } \ No newline at end of file