diff --git a/projecthearthstone.in/Courses/ASL course/Tree-graphic.png b/projecthearthstone.in/Courses/ASL course/Tree-graphic.png new file mode 100644 index 0000000..11a4b7b Binary files /dev/null and b/projecthearthstone.in/Courses/ASL course/Tree-graphic.png differ diff --git a/projecthearthstone.in/Courses/ASL course/course-data.json b/projecthearthstone.in/Courses/ASL course/course-data.json new file mode 100644 index 0000000..e376b58 --- /dev/null +++ b/projecthearthstone.in/Courses/ASL course/course-data.json @@ -0,0 +1,144 @@ +{ + "course": { + "title": "ASL Mastery", + "description": "American Sign Language from foundations to fluency" + }, + "sections": [ + { + "id": 1, + "key": "foundations", + "label": "Foundations", + "theme": "emerald", + "color": "#22dd88", + "portalColor": "34,221,136", + "description": "The core building blocks of ASL, including history, handshape, posture, fingerspelling, and expression.", + "videos": [ + { "id": "v1_1", "title": "What is ASL?", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v1_2", "title": "History of ASL", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v1_3", "title": "Deaf Culture and Community", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v1_4", "title": "How to Hold Your Hands and Body Posture", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v1_5", "title": "Fingerspelling A–M", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v1_6", "title": "Fingerspelling N–Z", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v1_7", "title": "Numbers 1–100", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v1_8", "title": "Eye Contact and Facial Expressions", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v1_9", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true } + ] + }, + { + "id": 2, + "key": "everyday-vocab", + "label": "Everyday Vocab", + "theme": "frost", + "color": "#4499ff", + "portalColor": "68,153,255", + "description": "Practical everyday vocabulary, including family, pronouns, places, people, and asking the six W questions.", + "locked": true, + "videos": [ + { "id": "v2_1", "title": "Family Signs (Naming)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v2_2", "title": "Pronouns and Indications", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v2_3", "title": "Describing People, Places, and Objects", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v2_4", "title": "At-Home Vocabulary", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v2_5", "title": "Cities and Public Places", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v2_6", "title": "Asking and Responding to the 6 W Questions", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v2_7", "title": "Responding to or Asking for Help (Emergency)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v2_8", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true } + ] + }, + { + "id": 3, + "key": "quantity-time", + "label": "Quantity & Time", + "theme": "gold", + "color": "#ffaa22", + "portalColor": "255,170,34", + "description": "Numbers up to 1000, reading clocks, relative time, seasons, years, and money.", + "locked": true, + "videos": [ + { "id": "v3_1", "title": "Numbers 1–1000", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v3_2", "title": "Reading and Interpreting Time on a Clock", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v3_3", "title": "Days, Weeks, and Months", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v3_4", "title": "Relative Time (Today, Tomorrow, Yesterday…)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v3_5", "title": "Seasons and Years", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v3_6", "title": "Money and Prices", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v3_7", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true } + ] + }, + { + "id": 4, + "key": "actions-feelings", + "label": "Actions & Feelings", + "theme": "hellfire", + "color": "#ff5533", + "portalColor": "255,85,51", + "description": "Common action words, emotions, facial grammar, body/health markers, adverbs, and question types.", + "locked": true, + "videos": [ + { "id": "v4_1", "title": "Common Action Words (Eat, Drink, Go…)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v4_2", "title": "Negation (No, Don't, Not)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v4_3", "title": "Emotions", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v4_4", "title": "Facial Indicators for Emotions", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v4_5", "title": "Body and Health Markers (Hungry, Tired, Sick…)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v4_6", "title": "Adverbs in ASL", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v4_7", "title": "Question Types: Yes/No vs. 6W Questions", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v4_8", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true } + ] + }, + { + "id": 5, + "key": "grammar", + "label": "ASL Grammar", + "theme": "toxic", + "color": "#66cc00", + "portalColor": "102,204,0", + "description": "ASL sentence structure, spatial references, tense, classifiers, perspective, rhetorics, and conditionals.", + "locked": true, + "videos": [ + { "id": "v5_1", "title": "ASL Sentence Structure (Topic-Comment)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v5_2", "title": "Setting Spatial References", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v5_3", "title": "Tense Types in ASL", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v5_4", "title": "Classifiers (Descriptive Signs)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v5_5", "title": "Perspective and Directionality (Reflexive Verbs)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v5_6", "title": "Rhetorical Questions", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v5_7", "title": "Conditionals in ASL", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v5_8", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true } + ] + }, + { + "id": 6, + "key": "conversation", + "label": "Conversation", + "theme": "blood", + "color": "#cc0022", + "portalColor": "180,0,34", + "description": "Opening and closing conversations, clarification, and real-world conversational contexts.", + "locked": true, + "videos": [ + { "id": "v6_1", "title": "Opening and Closing a Conversation", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v6_2", "title": "Clarifying, Repeating, and Asking Again", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v6_3", "title": "Medical Conversations (Vocab and Flow)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v6_4", "title": "Shopping Conversations", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v6_5", "title": "School and Sports Conversations", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v6_6", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true } + ] + }, + { + "id": 7, + "key": "fluency", + "label": "Fluency", + "theme": "arcane", + "color": "#aa55ff", + "portalColor": "140,60,255", + "description": "Regional variation, initialized signs, mouthing, speed reading, ASL literature, and the final challenge.", + "locked": true, + "boss": true, + "videos": [ + { "id": "v7_1", "title": "Regional and Stylistic Variation", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v7_2", "title": "Initialized Signs (F for Family, etc.)", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v7_3", "title": "Mouthing Words in ASL", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v7_4", "title": "Reading Fast Signers", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v7_5", "title": "ASL Literature and Its Importance", "youtube": "www.youtube.com", "duration": "" }, + { "id": "v7_6", "title": "Unit Test", "youtube": "www.youtube.com", "duration": "", "isTest": true } + ] + } + ] +} diff --git a/projecthearthstone.in/Courses/ASL course/course_video/course_video.html b/projecthearthstone.in/Courses/ASL course/course_video/course_video.html new file mode 100644 index 0000000..cf8d57a --- /dev/null +++ b/projecthearthstone.in/Courses/ASL course/course_video/course_video.html @@ -0,0 +1,104 @@ + + + + + + ASL Course - Playback + + + + + + +
+
ASL Mastery
+
Loading course…
+
+
+ + + + Lesson unlocked +
+ +
+ + + +
+
+ + +
+
+ + + + + + +
+
Lesson Locked
+
Complete the previous lesson to unlock this one.
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+ + +
+
+ +
+ + + + diff --git a/projecthearthstone.in/Courses/ASL course/course_video/script.js b/projecthearthstone.in/Courses/ASL course/course_video/script.js new file mode 100644 index 0000000..119dffa --- /dev/null +++ b/projecthearthstone.in/Courses/ASL course/course_video/script.js @@ -0,0 +1,339 @@ +(function() { + const canvas = document.getElementById('star-canvas'); + const ctx = canvas.getContext('2d'); + let W, H, stars = []; + function resize() { + W = canvas.width = window.innerWidth; + H = canvas.height = window.innerHeight; + stars = []; + const n = Math.floor(W * H / 2800); + for (let i = 0; i < n; i++) { + const t = Math.random(); + stars.push({ + x: Math.random()*W, y: Math.random()*H, + r: t>0.92?1.3+Math.random()*0.7:t>0.7?0.7+Math.random()*0.4:0.2+Math.random()*0.35, + a: 0.1+Math.random()*0.6, sp: 0.4+Math.random()*2.2, ph: Math.random()*Math.PI*2, + col: Math.random()>0.88?[180,200,255]:Math.random()>0.8?[200,160,255]:[215,215,238], + }); + } + } + window.addEventListener('resize', resize); resize(); + let f = 0; + (function draw() { + ctx.clearRect(0,0,W,H); + const t = f++ * 0.016; + stars.forEach(s => { + const al = s.a*(0.45+0.55*Math.sin(t*s.sp+s.ph)); + const [r,g,b] = s.col; + ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); + ctx.fillStyle=`rgba(${r},${g},${b},${al.toFixed(3)})`; ctx.fill(); + }); + requestAnimationFrame(draw); + })(); +})(); + +const LS_VIDEO_PROGRESS = 'asl_video_progress_v2'; +const LS_PORTAL_PROGRESS = 'asl_progress_v2'; + +function lsGet(key) { try { return localStorage.getItem(key); } catch(e) { return null; } } +function lsSet(key, val) { try { localStorage.setItem(key, val); } catch(e) {} } + +function loadCompletedVideos() { + try { + const raw = lsGet(LS_VIDEO_PROGRESS); + return raw ? JSON.parse(raw) : []; + } catch(e) { return []; } +} +function saveCompletedVideos(arr) { lsSet(LS_VIDEO_PROGRESS, JSON.stringify(arr)); } +function markVideoComplete(videoId) { + const done = loadCompletedVideos(); + if (!done.includes(videoId)) { done.push(videoId); saveCompletedVideos(done); } +} +function isVideoCompleted(videoId) { return loadCompletedVideos().includes(videoId); } + +function buildGlobalVideoList(sections) { + const list = []; + sections.forEach(sec => sec.videos.forEach(v => list.push({ ...v, sectionKey: sec.key }))); + return list; +} +function isVideoUnlocked(videoId, globalList) { + const idx = globalList.findIndex(v => v.id === videoId); + if (idx === 0) return true; + return isVideoCompleted(globalList[idx - 1].id); +} + +let courseData = null; +let currentSection = null; +let currentVideoIdx = 0; +let globalVideoList = []; + +function applyTheme(section) { + const root = document.documentElement; + root.style.setProperty('--col', section.color); + root.style.setProperty('--col-dim', hexToRgba(section.color, 0.15)); + const [r,g,b] = section.portalColor.split(',').map(Number); + root.style.setProperty('--col-shadow', `rgba(${r},${g},${b},0.18)`); + const name = document.getElementById('sidebar-section-name'); + name.textContent = section.label; + name.style.color = section.color; + name.style.textShadow = `0 0 28px ${section.color}`; + document.getElementById('sidebar-section-desc').textContent = section.description; + document.title = `ASL Mastery — ${section.label}`; +} + +function hexToRgba(hex, alpha) { + const r = parseInt(hex.slice(1,3),16); + const g = parseInt(hex.slice(3,5),16); + const b = parseInt(hex.slice(5,7),16); + return `rgba(${r},${g},${b},${alpha})`; +} + +function updateProgressBar() {} + +function buildSectionTabs(activeSection) { + const container = document.getElementById('section-tabs'); + container.innerHTML = ''; + courseData.sections.forEach(sec => { + const isLocked = !isVideoUnlocked(sec.videos[0].id, globalVideoList); + const isActive = sec.key === activeSection.key; + const btn = document.createElement('button'); + btn.className = 'section-tab' + (isActive ? ' active' : '') + (isLocked ? ' locked' : ''); + const dot = document.createElement('span'); + dot.className = 'tab-dot'; + dot.style.background = isLocked ? 'transparent' : sec.color; + dot.style.borderColor = sec.color; + dot.style.color = sec.color; + const label = document.createElement('span'); + label.className = 'tab-label'; + label.textContent = sec.label; + btn.appendChild(dot); + btn.appendChild(label); + if (isLocked) { + const lockIcon = document.createElement('span'); + lockIcon.className = 'tab-lock-icon'; + lockIcon.innerHTML = ` + + + + `; + btn.appendChild(lockIcon); + } else { + btn.addEventListener('click', () => switchSection(sec)); + } + container.appendChild(btn); + }); +} + +function buildVideoList(section, activeIdx) { + const container = document.getElementById('video-list'); + container.innerHTML = ''; + section.videos.forEach((video, i) => { + const unlocked = isVideoUnlocked(video.id, globalVideoList); + const completed = isVideoCompleted(video.id); + const isActive = i === activeIdx; + const btn = document.createElement('button'); + btn.className = 'video-item' + (isActive ? ' active' : '') + (!unlocked ? ' locked' : ''); + const num = document.createElement('span'); + num.className = 'video-num'; + num.textContent = String(i + 1).padStart(2, '0'); + const info = document.createElement('div'); + info.className = 'video-info'; + const title = document.createElement('div'); + title.className = 'video-title'; + title.textContent = video.title; + const dur = document.createElement('div'); + dur.className = 'video-dur'; + dur.textContent = video.duration; + info.appendChild(title); + info.appendChild(dur); + const statusWrap = document.createElement('div'); + statusWrap.className = 'video-status'; + if (!unlocked) { + statusWrap.innerHTML = ` + + + + `; + } else if (isActive) { + statusWrap.innerHTML = ``; + } else if (completed) { + statusWrap.innerHTML = ` + + `; + } else { + statusWrap.innerHTML = ` + + `; + } + btn.appendChild(num); + btn.appendChild(info); + btn.appendChild(statusWrap); + if (unlocked) btn.addEventListener('click', () => loadVideo(section, i)); + container.appendChild(btn); + }); +} + +const arrowSvg = ``; + +function loadVideo(section, idx) { + currentSection = section; + currentVideoIdx = idx; + const video = section.videos[idx]; + const unlocked = isVideoUnlocked(video.id, globalVideoList); + const frame = document.getElementById('video-frame'); + const lockedOverlay = document.getElementById('video-locked-overlay'); + const completeBtnWrap = document.getElementById('complete-btn-wrap'); + if (unlocked) { + const url = video.youtube.includes('?') + ? video.youtube + '&rel=0&modestbranding=1&enablejsapi=1' + : video.youtube + '?rel=0&modestbranding=1&enablejsapi=1'; + frame.src = url; + lockedOverlay.classList.remove('visible'); + completeBtnWrap.classList.remove('visible'); + if (!isVideoCompleted(video.id)) { + clearTimeout(window._completeBtnTimer); + window._completeBtnTimer = setTimeout(() => completeBtnWrap.classList.add('visible'), 3000); + } + } else { + frame.src = ''; + lockedOverlay.classList.add('visible'); + completeBtnWrap.classList.remove('visible'); + } + document.getElementById('current-video-title').textContent = video.title; + document.getElementById('current-video-meta').textContent = `${section.label} · ${video.duration}`; + const globalIdx = globalVideoList.findIndex(v => v.id === video.id); + const prevExists = globalIdx > 0; + const nextExists = globalIdx < globalVideoList.length - 1; + const nextUnlocked = nextExists && isVideoUnlocked(globalVideoList[globalIdx+1].id, globalVideoList); + document.getElementById('btn-prev-vid').disabled = !prevExists; + const nextBtn = document.getElementById('btn-next-vid'); + nextBtn.disabled = !nextExists || !nextUnlocked; + if (nextExists) { + const nextVid = globalVideoList[globalIdx + 1]; + if (nextVid.sectionKey !== section.key) { + const nextSec = courseData.sections.find(s => s.key === nextVid.sectionKey); + nextBtn.innerHTML = `Next Section: ${nextSec ? nextSec.label : 'Next'} ${arrowSvg}`; + } else { + nextBtn.innerHTML = `Next Lesson ${arrowSvg}`; + } + } else { + nextBtn.innerHTML = `Next Lesson ${arrowSvg}`; + } + lsSet('asl_current_section', section.key); + lsSet('asl_current_video', video.id); + buildVideoList(section, idx); + buildSectionTabs(section); +} + +function completeCurrentLesson() { + const video = currentSection.videos[currentVideoIdx]; + markVideoComplete(video.id); + document.getElementById('complete-btn-wrap').classList.remove('visible'); + const globalIdx = globalVideoList.findIndex(v => v.id === video.id); + let unlockedLabel = null; + let unlockedSection = null; + if (globalIdx < globalVideoList.length - 1) { + const nextVid = globalVideoList[globalIdx + 1]; + if (nextVid.sectionKey !== currentSection.key) { + const nextSec = courseData.sections.find(s => s.key === nextVid.sectionKey); + unlockedLabel = `Section unlocked: ${nextSec ? nextSec.label : 'Next'}`; + unlockedSection = nextSec || null; + const secIdx = courseData.sections.findIndex(s => s.key === nextVid.sectionKey); + const current = parseInt(lsGet(LS_PORTAL_PROGRESS) || '1', 10); + if (secIdx + 1 > current) lsSet(LS_PORTAL_PROGRESS, String(secIdx + 1)); + } + } + buildVideoList(currentSection, currentVideoIdx); + buildSectionTabs(currentSection); + const vidGlobalIdx = globalVideoList.findIndex(v => v.id === video.id); + if (vidGlobalIdx < globalVideoList.length - 1) { + document.getElementById('btn-next-vid').disabled = false; + } + if (unlockedLabel) showToast(unlockedLabel, unlockedSection); +} + +function showToast(text, section) { + const toast = document.getElementById('unlock-toast'); + document.getElementById('unlock-toast-text').textContent = text; + const color = section ? section.color : 'rgba(170,85,255,1)'; + const [r, g, b] = section ? section.portalColor.split(',').map(Number) : [170, 85, 255]; + toast.style.borderColor = color; + toast.style.color = color; + toast.style.background = `rgba(${r},${g},${b},0.08)`; + toast.style.boxShadow = `0 0 32px rgba(${r},${g},${b},0.28), 0 8px 32px rgba(0,0,0,0.6)`; + toast.classList.add('show'); + setTimeout(() => toast.classList.remove('show'), 3500); +} + +function switchSection(section) { + applyTheme(section); + const savedVideoId = lsGet('asl_current_video'); + const savedIdx = section.videos.findIndex(v => v.id === savedVideoId); + const targetIdx = (savedIdx >= 0 && section.key === lsGet('asl_current_section')) ? savedIdx : 0; + buildVideoList(section, targetIdx); + loadVideo(section, targetIdx); + const url = new URL(window.location); + url.searchParams.set('section', section.key); + window.history.replaceState({}, '', url); +} + +window.addEventListener('message', (e) => { + try { + const data = JSON.parse(e.data); + if (data.event === 'onStateChange' && data.info === 0) { + document.getElementById('complete-btn-wrap').classList.add('visible'); + } + } catch(e) {} +}); + +document.getElementById('btn-prev-vid').addEventListener('click', () => { + const globalIdx = globalVideoList.findIndex(v => v.id === currentSection.videos[currentVideoIdx].id); + if (globalIdx > 0) { + const prev = globalVideoList[globalIdx - 1]; + const prevSec = courseData.sections.find(s => s.key === prev.sectionKey); + const prevVidIdx = prevSec.videos.findIndex(v => v.id === prev.id); + if (prevSec.key !== currentSection.key) applyTheme(prevSec); + buildVideoList(prevSec, prevVidIdx); + buildSectionTabs(prevSec); + loadVideo(prevSec, prevVidIdx); + } +}); + +document.getElementById('btn-next-vid').addEventListener('click', () => { + const globalIdx = globalVideoList.findIndex(v => v.id === currentSection.videos[currentVideoIdx].id); + if (globalIdx < globalVideoList.length - 1) { + const next = globalVideoList[globalIdx + 1]; + if (!isVideoUnlocked(next.id, globalVideoList)) return; + const nextSec = courseData.sections.find(s => s.key === next.sectionKey); + const nextVidIdx = nextSec.videos.findIndex(v => v.id === next.id); + if (nextSec.key !== currentSection.key) applyTheme(nextSec); + buildSectionTabs(nextSec); + buildVideoList(nextSec, nextVidIdx); + loadVideo(nextSec, nextVidIdx); + } +}); + +document.getElementById('complete-btn').addEventListener('click', completeCurrentLesson); + +async function init() { + courseData = await (await fetch('../course-data.json')).json(); + globalVideoList = buildGlobalVideoList(courseData.sections); + const params = new URLSearchParams(window.location.search); + const urlSection = params.get('section'); + const savedSection = lsGet('asl_current_section'); + const savedVideo = lsGet('asl_current_video'); + let section = courseData.sections.find(s => s.key === (urlSection || savedSection)) || courseData.sections[0]; + if (!isVideoUnlocked(section.videos[0].id, globalVideoList)) section = courseData.sections[0]; + let vidIdx = 0; + if (savedVideo) { + const found = section.videos.findIndex(v => v.id === savedVideo); + if (found >= 0 && isVideoUnlocked(section.videos[found].id, globalVideoList)) vidIdx = found; + } + applyTheme(section); + buildSectionTabs(section); + buildVideoList(section, vidIdx); + loadVideo(section, vidIdx); + document.getElementById('loading-overlay').classList.add('hidden'); +} + +init(); diff --git a/projecthearthstone.in/Courses/ASL course/course_video/style.css b/projecthearthstone.in/Courses/ASL course/course_video/style.css new file mode 100644 index 0000000..1975425 --- /dev/null +++ b/projecthearthstone.in/Courses/ASL course/course_video/style.css @@ -0,0 +1,330 @@ +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } + +:root { + --col: #aa55ff; + --col-dim: rgba(170,85,255,0.18); + --col-shadow: rgba(140,60,255,0.12); + --bg: #06060b; + --sidebar-w: 300px; + --bottom-h: 86px; +} + +html, body { + width: 100%; height: 100%; + background: var(--bg); + color: rgba(255,255,255,0.85); + overflow: hidden; +} + +#star-canvas { + position: fixed; inset: 0; + pointer-events: none; z-index: 0; +} + +#app { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + z-index: 1; + display: grid; + grid-template-columns: var(--sidebar-w) 1fr; + grid-template-rows: 1fr var(--bottom-h); +} + +#sidebar { + grid-row: 1 / 3; + background: rgba(4,4,9,0.7); + border-right: 1px solid rgba(255,255,255,0.07); + display: flex; flex-direction: column; + overflow: hidden; +} + +#sidebar-header { + padding: 16px 20px; + border-bottom: 1px solid rgba(255,255,255,0.06); + flex-shrink: 0; + display: flex; flex-direction: column; +} + +#btn-tree { + display: flex; align-items: center; gap: 8px; + padding: 8px 12px; margin-bottom: 16px; + background: rgba(255,255,255,0.05); + border: 1px solid rgba(255,255,255,0.12); + border-radius: 9px; + color: rgba(255,255,255,0.45); + font-size: 9px; letter-spacing: 0.28em; text-transform: uppercase; + text-decoration: none; cursor: pointer; + transition: background 0.2s, color 0.2s, border-color 0.2s; + width: fit-content; +} +#btn-tree:hover { + background: rgba(255,255,255,0.1); + border-color: rgba(255,255,255,0.3); + color: rgba(255,255,255,0.82); +} +#btn-tree svg { flex-shrink: 0; color: inherit; } + +#sidebar-section-name { + font-size: 16px; font-weight: 700; + letter-spacing: 0.12em; text-transform: uppercase; + color: var(--col); + text-shadow: 0 0 28px var(--col); + margin-bottom: 7px; +} +#sidebar-section-desc { + font-size: 13px; line-height: 1.55; + color: rgba(255,255,255,0.32); + font-style: italic; +} + +#section-tabs { + padding: 8px 10px 6px; + border-bottom: 1px solid rgba(255,255,255,0.05); + flex-shrink: 0; + display: flex; flex-direction: column; gap: 1px; +} + +.section-tab { + display: flex; align-items: center; gap: 10px; + padding: 9px 12px; border-radius: 7px; + cursor: pointer; transition: background 0.18s, opacity 0.18s; + border: none; background: none; width: 100%; text-align: left; + position: relative; +} +.section-tab:not(.locked):hover { background: rgba(255,255,255,0.05); } +.section-tab.active { background: rgba(255,255,255,0.07); } +.section-tab.active::before { + content: ''; + position: absolute; left: 0; top: 20%; bottom: 20%; + width: 2px; border-radius: 2px; + background: var(--col); + box-shadow: 0 0 8px var(--col); +} +.section-tab.locked { opacity: 0.28; cursor: not-allowed; pointer-events: none; } + +.tab-dot { + width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; + border: 1.5px solid rgba(255,255,255,0.2); + transition: box-shadow 0.2s; +} +.section-tab.active .tab-dot { box-shadow: 0 0 7px currentColor; } +.section-tab.locked .tab-dot { background: transparent !important; } + +.tab-label { + font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; + color: rgba(255,255,255,0.45); flex: 1; +} +.section-tab.active .tab-label { color: rgba(255,255,255,0.88); } + +.tab-lock-icon { flex-shrink: 0; opacity: 0.5; } +.tab-lock-icon svg { display: block; } + +#video-list { + flex: 1; overflow-y: auto; + padding: 10px; + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.08) transparent; + display: flex; flex-direction: column; gap: 2px; +} + +.video-item { + display: flex; align-items: center; gap: 12px; + padding: 12px; border-radius: 8px; + cursor: pointer; border: none; background: none; + width: 100%; text-align: left; + transition: background 0.18s, opacity 0.18s; +} +.video-item:not(.locked):hover { background: rgba(255,255,255,0.06); } +.video-item.active { background: rgba(255,255,255,0.09); } +.video-item.locked { opacity: 0.3; cursor: not-allowed; pointer-events: none; } + +.video-num { + font-size: 10px; color: rgba(255,255,255,0.18); + min-width: 22px; letter-spacing: 0.05em; flex-shrink: 0; +} +.video-item.active .video-num { color: var(--col); } + +.video-info { flex: 1; min-width: 0; } +.video-title { + font-size: 14px; font-weight: 400; + color: rgba(255,255,255,0.6); + line-height: 1.3; margin-bottom: 3px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.video-item.active .video-title { color: rgba(255,255,255,0.95); } +.video-dur { + font-size: 9px; color: rgba(255,255,255,0.18); +} + +.video-status { flex-shrink: 0; display: flex; align-items: center; } +.video-item.active .vstat-dot { + width: 6px; height: 6px; border-radius: 50%; + background: var(--col); box-shadow: 0 0 8px var(--col); + animation: vidPulse 1.8s ease-in-out infinite; +} +@keyframes vidPulse { 0%,100%{opacity:1} 50%{opacity:0.4} } + +.vstat-check { color: rgba(255,255,255,0.25); } +.vstat-check.done { color: var(--col); } +.vstat-lock { opacity: 0.4; } + +#main { + grid-row: 1 / 2; + display: flex; flex-direction: column; + overflow: hidden; + background: rgba(0,0,0,0.1); + padding: 24px 32px 16px; +} + +#video-frame-wrap { + flex: 1; + border-radius: 12px; + overflow: hidden; + box-shadow: + 0 0 0 1px rgba(255,255,255,0.08), + 0 8px 48px rgba(0,0,0,0.7), + 0 0 80px var(--col-shadow); + background: #000; + position: relative; +} +#video-frame { width: 100%; height: 100%; border: none; display: block; } + +#video-locked-overlay { + position: absolute; inset: 0; + background: rgba(4,4,10,0.88); + display: none; + flex-direction: column; + align-items: center; justify-content: center; + gap: 16px; z-index: 5; + backdrop-filter: blur(4px); +} +#video-locked-overlay.visible { display: flex; } +.locked-overlay-icon { opacity: 0.5; } +.locked-overlay-title { + font-size: 16px; letter-spacing: 0.3em; text-transform: uppercase; + color: rgba(255,255,255,0.3); +} +.locked-overlay-hint { + font-size: 13px; font-style: italic; + color: rgba(255,255,255,0.18); + max-width: 320px; text-align: center; line-height: 1.5; +} + +#complete-btn-wrap { + position: absolute; bottom: 20px; right: 20px; + z-index: 6; opacity: 0; pointer-events: none; + transition: opacity 0.4s; +} +#complete-btn-wrap.visible { opacity: 1; pointer-events: all; } +#complete-btn { + display: flex; align-items: center; gap: 8px; + background: var(--col); + border: none; border-radius: 10px; + padding: 12px 22px; + color: #fff; + font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; + cursor: pointer; + box-shadow: 0 0 24px var(--col-shadow), 0 4px 16px rgba(0,0,0,0.5); + transition: transform 0.15s, box-shadow 0.15s; +} +#complete-btn:hover { transform: scale(1.04); box-shadow: 0 0 36px var(--col), 0 4px 20px rgba(0,0,0,0.5); } +#complete-btn:active { transform: scale(0.97); } + +#bottom-bar { + grid-column: 2; grid-row: 2; + display: flex; align-items: center; + padding: 0 28px; gap: 16px; + border-top: 1px solid rgba(255,255,255,0.07); + background: rgba(4,4,9,0.82); + backdrop-filter: blur(14px); +} + +#bottom-bar-left { + display: flex; flex-direction: column; gap: 3px; + flex: 1; min-width: 0; +} + +#current-video-title { + font-size: 15px; font-weight: 600; + letter-spacing: 0.06em; + color: rgba(255,255,255,0.88); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + max-width: 380px; +} +#current-video-meta { + font-size: 12px; font-style: italic; + color: rgba(255,255,255,0.3); + white-space: nowrap; +} + +#video-nav { margin-left: auto; display: flex; align-items: center; gap: 12px; flex-shrink: 0; } + +.vnav-btn { + display: flex; align-items: center; gap: 10px; + background: rgba(255,255,255,0.07); + border: 1px solid rgba(255,255,255,0.18); + border-radius: 12px; padding: 13px 26px; + color: rgba(255,255,255,0.65); + font-size: 12px; font-weight: 600; + letter-spacing: 0.12em; text-transform: uppercase; + cursor: pointer; transition: all 0.18s; + white-space: nowrap; min-width: 148px; justify-content: center; +} +.vnav-btn:hover:not(:disabled) { + background: rgba(255,255,255,0.13); + color: rgba(255,255,255,0.95); + border-color: rgba(255,255,255,0.4); + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(0,0,0,0.3); +} +.vnav-btn:active:not(:disabled) { transform: translateY(0); } +.vnav-btn:disabled { opacity: 0.2; cursor: default; } + +.vnav-btn.primary { + background: rgba(170,85,255,0.12); + border-color: var(--col); + color: var(--col); + box-shadow: 0 0 20px var(--col-shadow); +} +.vnav-btn.primary:hover:not(:disabled) { + background: var(--col-dim); + box-shadow: 0 0 28px var(--col-shadow), 0 4px 20px rgba(0,0,0,0.4); + color: #fff; + border-color: var(--col); +} + +#loading-overlay { + position: fixed; inset: 0; z-index: 200; + background: var(--bg); + display: flex; align-items: center; justify-content: center; + flex-direction: column; gap: 20px; + transition: opacity 0.6s; +} +#loading-overlay.hidden { opacity: 0; pointer-events: none; } +.loading-glyph { + font-size: 32px; letter-spacing: 0.5em; text-transform: uppercase; + color: rgba(255,255,255,0.07); + animation: loadPulse 2s ease-in-out infinite; +} +.loading-sub { + font-size: 9px; letter-spacing: 0.4em; + color: rgba(255,255,255,0.12); text-transform: uppercase; +} +@keyframes loadPulse { 0%,100%{opacity:0.07} 50%{opacity:0.4} } + +#unlock-toast { + position: fixed; bottom: 90px; left: 50%; + transform: translateX(-50%) translateY(20px); + background: rgba(10,10,18,0.95); + border: 1px solid var(--col); + border-radius: 12px; padding: 14px 24px; + display: flex; align-items: center; gap: 12px; + font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; + color: var(--col); + box-shadow: 0 0 32px var(--col-shadow), 0 8px 32px rgba(0,0,0,0.6); + z-index: 300; + opacity: 0; pointer-events: none; + transition: opacity 0.4s, transform 0.4s; + white-space: nowrap; +} +#unlock-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } diff --git a/projecthearthstone.in/Courses/ASL course/index.html b/projecthearthstone.in/Courses/ASL course/index.html new file mode 100644 index 0000000..2320eeb --- /dev/null +++ b/projecthearthstone.in/Courses/ASL course/index.html @@ -0,0 +1,92 @@ + + + + + + ASL Mastery — Course Map + + + + + + + + +
+
+
+ +
+
ASL Mastery · Course Map
+ +
+
+
+
+
+
+
+
+
+ + + +
+ +
+
+ + Tree + + + +
+
+
+
+
+
+
+ +
+
+ + + + + diff --git a/projecthearthstone.in/Courses/ASL course/script.js b/projecthearthstone.in/Courses/ASL course/script.js new file mode 100644 index 0000000..a80294b --- /dev/null +++ b/projecthearthstone.in/Courses/ASL course/script.js @@ -0,0 +1,789 @@ +let COURSE_DATA = null; + +async function loadCourseData() { + try { + const res = await fetch('course-data.json'); + COURSE_DATA = await res.json(); + } catch(e) {} +} + +const STORAGE_KEY = 'asl_progress_v2'; +const TOTAL_PORTALS = 7; + +function loadProgress() { + try { + const n = parseInt(localStorage.getItem(STORAGE_KEY), 10); + return isNaN(n) ? 1 : Math.max(1, Math.min(n, TOTAL_PORTALS)); + } catch(e) { return 1; } +} + +function saveProgress(count) { + try { localStorage.setItem(STORAGE_KEY, String(count)); } catch(e) {} +} + +let unlockedCount = loadProgress(); + +const THEMES = { + arcane: { dark:'#0b0220', mid:'#7733cc', bright:'#cc99ff', edge:'#aa55ff', edgeM:'#cc99ff', flash:'rgba(140,60,255,0.22)' }, + emerald: { dark:'#011510', mid:'#11aa66', bright:'#66ffcc', edge:'#22dd88', edgeM:'#77ffcc', flash:'rgba(20,180,100,0.20)' }, + hellfire:{ dark:'#160303', mid:'#cc3311', bright:'#ffaa77', edge:'#ff5533', edgeM:'#ffaa77', flash:'rgba(220,60,20,0.22)' }, + frost: { dark:'#010d18', mid:'#2266cc', bright:'#99ccff', edge:'#4499ff', edgeM:'#aaddff', flash:'rgba(50,130,255,0.22)' }, + gold: { dark:'#120c00', mid:'#cc8800', bright:'#ffdd77', edge:'#ffaa22', edgeM:'#ffe088', flash:'rgba(220,160,20,0.22)' }, + abyss: { dark:'#000510', mid:'#003366', bright:'#4499cc', edge:'#1166aa', edgeM:'#66bbee', flash:'rgba(10,80,160,0.22)' }, + toxic: { dark:'#060f02', mid:'#44aa00', bright:'#aaff44', edge:'#66cc00', edgeM:'#bbff55', flash:'rgba(90,200,10,0.22)' }, + blood: { dark:'#120002', mid:'#990011', bright:'#ff4455', edge:'#cc0022', edgeM:'#ff6677', flash:'rgba(200,10,30,0.22)' }, + solar: { dark:'#130800', mid:'#dd5500', bright:'#ffcc44', edge:'#ff8800', edgeM:'#ffdd66', flash:'rgba(240,130,20,0.24)' }, + void: { dark:'#050508', mid:'#333355', bright:'#9999cc', edge:'#555577', edgeM:'#aaaacc', flash:'rgba(80,80,140,0.22)' }, +}; + +function hash(n) { + const x = Math.sin(n * 127.1 + 311.7) * 43758.5453; + return x - Math.floor(x); +} + +function tornEdge(ax, ay, bx, by, nx, ny, depth, count, seed, si) { + const pts = []; + for (let i = 0; i < count; i++) { + const t = (i + 0.5) / count; + const px = ax + (bx-ax)*t, py = ay + (by-ay)*t; + const h1 = hash(seed + si*100 + i*7.3); + const h2 = hash(seed + si*100 + i*13.7 + 5); + const h3 = hash(seed + si*100 + i*3.1 + 9); + let amp; + if (h1 > 0.65) amp = depth * (0.6 + h2 * 0.8); + else if (h1 > 0.35) amp = depth * (0.1 + h2 * 0.4) * (h3 > 0.5 ? 1 : -0.7); + else amp = -depth * (0.2 + h2 * 0.5); + pts.push(px + nx*amp, py + ny*amp); + } + return pts; +} + +function jaggedPoly(cx, cy, size, depth, density, seed) { + const h = size/2; + const corners = [[cx-h,cy-h],[cx+h,cy-h],[cx+h,cy+h],[cx-h,cy+h]]; + const all = []; + for (let s = 0; s < 4; s++) { + const [ax,ay] = corners[s], [bx,by] = corners[(s+1)%4]; + const ex=bx-ax, ey=by-ay, el=Math.hypot(ex,ey); + let nx=-ey/el, ny=ex/el; + if (((ax+bx)/2-cx)*nx + ((ay+by)/2-cy)*ny < 0) { nx=-nx; ny=-ny; } + all.push(ax, ay); + for (const p of tornEdge(ax,ay,bx,by,nx,ny,depth,density,seed,s)) all.push(p); + } + const s=[]; + for (let i=0;i rgbA(mr,mg,mb,a); + const brightStr = (a) => rgbA(br2,bg2,bb2,a); + const darkStr = (a) => rgbA(dr,dg,db,a); + const swirlOff = document.createElement('canvas'); + swirlOff.width=W; swirlOff.height=H; + const sctx = swirlOff.getContext('2d'); + const vigOff = document.createElement('canvas'); + vigOff.width=W; vigOff.height=H; + const vctx = vigOff.getContext('2d'); + (function bakeVig(){ + const v = vctx.createRadialGradient(CX,CY,42, CX,CY,124); + v.addColorStop(0,'rgba(0,0,0,0)'); + v.addColorStop(0.6,'rgba(0,0,0,0.16)'); + v.addColorStop(1,'rgba(0,0,0,0.72)'); + vctx.fillStyle=v; vctx.fillRect(0,0,W,H); + })(); + let hoverT=0, burstT=0, swirlBoost=1, isHovered=false; + const wisps = Array.from({length:18},(_,i)=>({ + angle:(i/18)*Math.PI*2, radius:16+(i%6)*14, speed:0.007+(i%5)*0.003, + size:7+(i%6)*5, alpha:0.18+(i%4)*0.08, bright:i%3===0, + })); + function tick(frame) { + const t = frame * 0.016; + hoverT += ((isHovered?1:0) - hoverT) * 0.07; + if (burstT > 0.01) { swirlBoost = 1 + burstT*5; burstT *= 0.88; } + else { swirlBoost += (1-swirlBoost)*0.04; } + const hB = 1 + hoverT*1.1; + ctx.fillStyle = darkStr(1); ctx.fillRect(0,0,W,H); + if (hoverT > 0.02) { + const ag = ctx.createRadialGradient(CX,CY,58, CX,CY,152); + ag.addColorStop(0, midStr(0)); ag.addColorStop(0.5, midStr(0.06*hoverT)); ag.addColorStop(1, darkStr(0)); + ctx.fillStyle = ag; ctx.fillRect(0,0,W,H); + } + sctx.clearRect(0,0,W,H); + for (let arm=0; arm<3; arm++) { + const off2=(arm/3)*Math.PI*2, rot=0.22+arm*0.09; + const isBright = arm===1; + for (let step=0; step<55; step++) { + const frac=step/55; + const angle=off2+frac*Math.PI*3.8+t*rot*swirlBoost; + const dist=frac*95; + const wx=CX+Math.cos(angle)*dist, wy=CY+Math.sin(angle)*dist; + const sz=(1-frac)*28+3; + const al=(1-frac)*(0.10+hoverT*0.11)*hB+(arm===0?0.018:0); + const wg=sctx.createRadialGradient(wx,wy,0, wx,wy,sz); + wg.addColorStop(0, isBright ? brightStr(al) : midStr(al)); + wg.addColorStop(1, midStr(0)); + sctx.fillStyle=wg; + sctx.beginPath(); sctx.arc(wx,wy,sz,0,Math.PI*2); sctx.fill(); + } + } + ctx.drawImage(swirlOff,0,0); + wisps.forEach((w,i) => { + const dir=i%2===0?1:-1; + const angle=w.angle+t*w.speed*dir*(Math.floor(i/6)+1)*1.6*swirlBoost; + const r2=w.radius+Math.sin(t*0.9+i)*9; + const wx=CX+Math.cos(angle)*r2, wy=CY+Math.sin(angle)*r2; + const sz=w.size*(0.75+Math.sin(t+i*0.7)*0.25)*(1+hoverT*0.35); + const al=w.alpha*hB*(0.65+Math.sin(t*1.3+i)*0.35); + const wg=ctx.createRadialGradient(wx,wy,0, wx,wy,sz); + wg.addColorStop(0, w.bright ? brightStr(al) : midStr(al)); + wg.addColorStop(1, midStr(0)); + ctx.fillStyle=wg; + ctx.beginPath(); ctx.arc(wx,wy,sz,0,Math.PI*2); ctx.fill(); + }); + ctx.drawImage(vigOff,0,0); + const pulse=0.78+Math.sin(t*2.2)*0.22; + const coreR=(42+hoverT*14)*pulse; + const cg=ctx.createRadialGradient(CX,CY,0, CX,CY,coreR); + cg.addColorStop(0, brightStr((0.65+hoverT*0.25)*pulse*hB)); + cg.addColorStop(0.28, midStr(0.3)); + cg.addColorStop(0.65, midStr(0.07)); + cg.addColorStop(1, midStr(0)); + ctx.fillStyle=cg; ctx.fillRect(0,0,W,H); + const pr=(4+hoverT*3)*pulse; + const pp=ctx.createRadialGradient(CX,CY,0, CX,CY,pr*3.5); + pp.addColorStop(0,'rgba(255,255,255,1)'); + pp.addColorStop(0.35, brightStr(0.85)); + pp.addColorStop(1, brightStr(0)); + ctx.fillStyle=pp; ctx.fillRect(0,0,W,H); + if (burstT > 0.01) { + const bf=ctx.createRadialGradient(CX,CY,0, CX,CY,135); + bf.addColorStop(0, brightStr(Math.min(burstT*1.1,1))); + bf.addColorStop(0.15, brightStr(burstT*0.9)); + bf.addColorStop(0.45, midStr(burstT*0.45)); + bf.addColorStop(1, midStr(0)); + ctx.fillStyle=bf; ctx.fillRect(0,0,W,H); + } + } + _tickers.push(tick); + return { setHovered(v){isHovered=v;}, burst(){burstT=1;} }; +} + +let _uid = 0; +function initPortal(wrapEl) { + const id = ++_uid; + const theme = wrapEl.dataset.theme || 'arcane'; + const cfg = THEMES[theme] || THEMES.arcane; + const depth = 24; + const isTop = wrapEl.classList.contains('boss'); + const isLocked = wrapEl.classList.contains('locked'); + const polySize = isTop ? 190 : 155; + const svgSize = isTop ? 280 : 240; + + if (isLocked) { + const lid = id; + const lockedLabel = wrapEl.dataset.label || 'Locked'; + const pts = jaggedPoly(120,120,polySize,depth,14,42.7); + wrapEl.innerHTML = ` + + + + + + + + + + + + + + + + + + ${lockedLabel} + + `; + ['cp','ibg','em','eh'].forEach(pfx => { + const el = document.getElementById(pfx+lid); + if (el) el.setAttribute('points', pts); + }); + return; + } + + const allWrapsNow = Array.from(document.querySelectorAll('.portal-wrap')); + const portalIdx = allWrapsNow.indexOf(wrapEl); + const section = COURSE_DATA && COURSE_DATA.sections ? COURSE_DATA.sections[portalIdx] : null; + const sectionLabel = section ? section.label : (wrapEl.dataset.label || theme); + const sectionColor = section ? section.color : cfg.bright; + const sectionKey = section ? section.key : String(portalIdx + 1); + + wrapEl.innerHTML = ` + + + + + + + + + + + + + + + + + + + + + + + + ${sectionLabel} + + `; + + const svg = document.getElementById('sv'+id); + const cp = document.getElementById('cp'+id); + const ha = document.getElementById('ha'+id); + const eg = document.getElementById('eg'+id); + const em = document.getElementById('em'+id); + const eh = document.getElementById('eh'+id); + const canvas = document.getElementById('cv'+id); + + ha.setAttribute('stroke', cfg.bright); + eg.setAttribute('stroke', cfg.edge); + em.setAttribute('stroke', cfg.edgeM); + + let seedA=Math.random()*999, seedB=seedA+40+Math.random()*60; + let pA=jaggedPoly(120,120,polySize,28,14,seedA); + let pB=jaggedPoly(120,120,polySize,28,14,seedB); + let mT=0, hov=false; + let svgScale=1, svgScaleTarget=1; + let haOpacity=0, haOpacityTarget=0; + let edgeGlowW=7, edgeGlowWTarget=7; + let clickPhase='idle', clickT=0; + + function ss(x){return x*x*(3-2*x);} + function lerp(a,b,t){return a+(b-a)*t;} + function interpPts(a,b,t){ + const av=a.split(' ').map(s=>s.split(',').map(Number)); + const bv=b.split(' ').map(s=>s.split(',').map(Number)); + return av.map(([ax,ay],i)=> + ((ax+(bv[i][0]-ax)*t).toFixed(1)+','+(ay+(bv[i][1]-ay)*t).toFixed(1)) + ).join(' '); + } + function setPoly(pts){ [cp,ha,eg,em,eh].forEach(el=>el.setAttribute('points',pts)); } + setPoly(pA); + + function animLoop(frame){ + if (frame % 3 === 0) { + mT += hov ? 0.038 : 0.028; + if (mT>=1){ + mT=0; seedA=seedB; pA=pB; + seedB=seedA+25+Math.random()*45; + pB=jaggedPoly(120,120,polySize, hov?36:28, 14, seedB); + } + setPoly(interpPts(pA,pB,ss(mT))); + } + if (clickPhase==='compress'){ + clickT+=0.18; svgScaleTarget=lerp(1,0.82,Math.min(clickT,1)); + if(clickT>=1){clickPhase='explode';clickT=0;} + } else if (clickPhase==='explode'){ + clickT+=0.14; svgScaleTarget=lerp(0.82,1.22,ss(Math.min(clickT,1))); + edgeGlowWTarget=7+(1-clickT)*18; + if(clickT>=1){clickPhase='settle';clickT=0;} + } else if (clickPhase==='settle'){ + clickT+=0.06; svgScaleTarget=lerp(1.22,1,ss(Math.min(clickT,1))); + if(clickT>=1){clickPhase='idle'; svgScaleTarget=hov?1.07:1;} + } else { + svgScaleTarget = hov ? 1.07 : 1; + } + const ambientHalo = 0.12 + Math.sin(Date.now()*0.0018 + id)*0.06; + haOpacityTarget = hov ? (0.42+Math.sin(Date.now()*0.003)*0.15) : ambientHalo; + svgScale = lerp(svgScale, svgScaleTarget, 0.12); + haOpacity = lerp(haOpacity, haOpacityTarget, 0.06); + edgeGlowW = lerp(edgeGlowW, edgeGlowWTarget, 0.12); + svg.style.transform = `scale(${svgScale.toFixed(4)})`; + const glowSize = hov ? (haOpacity*32).toFixed(1) : (ambientHalo*16).toFixed(1); + svg.style.filter = hov + ? `brightness(${(1.15+haOpacity*0.3).toFixed(3)}) drop-shadow(0 0 ${glowSize}px ${cfg.edge})` + : `brightness(1.04) drop-shadow(0 0 ${glowSize}px ${cfg.edge})`; + ha.setAttribute('opacity', haOpacity.toFixed(3)); + eg.setAttribute('stroke-width', edgeGlowW.toFixed(1)); + } + _tickers.push(animLoop); + + const ren = makeRenderer(canvas, cfg); + + svg.addEventListener('mouseenter', ()=>{ + hov=true; wrapEl.classList.add('hovered'); ren.setHovered(true); + seedB=seedA+20+Math.random()*35; + pB=jaggedPoly(120,120,polySize,32,14,seedB); + }); + svg.addEventListener('mouseleave', ()=>{ + hov=false; wrapEl.classList.remove('hovered'); ren.setHovered(false); + seedB=seedA+30+Math.random()*50; + pB=jaggedPoly(120,120,polySize,depth,14,seedB); + }); + svg.addEventListener('click', ()=>{ + if (clickPhase!=='idle') return; + clickPhase='compress'; clickT=0; + ren.burst(); + flashOverlay.style.background = cfg.flash; + flashOverlay.classList.remove('flash'); + void flashOverlay.offsetWidth; + flashOverlay.classList.add('flash'); + document.body.style.transform='translate(3px,-2px)'; + setTimeout(()=>{document.body.style.transform='translate(-2px,3px)';},40); + setTimeout(()=>{document.body.style.transform='translate(2px,-1px)';},80); + setTimeout(()=>{document.body.style.transform='translate(0,0)';},120); + try { localStorage.setItem('asl_current_section', sectionKey); } catch(e) {} + setTimeout(() => { + window.location.href = `course_video/course_video.html?section=${sectionKey}`; + }, 480); + }); +} + +function unlockNextPortal() { + const wraps = Array.from(document.querySelectorAll('.portal-wrap')); + const nextWrap = wraps[unlockedCount]; + if (!nextWrap || !nextWrap.classList.contains('locked')) return; + + nextWrap.style.opacity = '0'; + nextWrap.style.transition = 'opacity 0.9s ease, transform 0.9s cubic-bezier(0.34,1.56,0.64,1)'; + nextWrap.style.transform = 'translate(-50%,-50%) scale(0.4)'; + nextWrap.classList.remove('locked'); + nextWrap.innerHTML = ''; + initPortal(nextWrap); + + requestAnimationFrame(() => requestAnimationFrame(() => { + nextWrap.style.opacity = '1'; + nextWrap.style.transform = 'translate(-50%,-50%) scale(1)'; + })); + + unlockedCount++; + saveProgress(unlockedCount); + updateProgressDots(); + + const theme = nextWrap.dataset.theme || 'arcane'; + const cfg = THEMES[theme] || THEMES.arcane; + setTimeout(() => { + flashOverlay.style.background = cfg.flash; + flashOverlay.classList.remove('flash'); + void flashOverlay.offsetWidth; + flashOverlay.classList.add('flash'); + }, 200); +} + +function applyInitialLockState() { + document.querySelectorAll('.portal-wrap').forEach((wrap, i) => { + wrap.classList.toggle('locked', i >= unlockedCount); + }); +} + +function updateProgressDots() { + document.querySelectorAll('.pdot').forEach((dot, i) => { + dot.classList.toggle('on', i < unlockedCount); + }); +} + +const WORLD_W = 3200; +const WORLD_H = 3200; +const MM_W = 88; +const MM_H = 88; + +const PORTALS = [ + { x: 700, y: 2680, color:'34,221,136', label:'Foundations' }, + { x: 2780, y: 2620, color:'68,153,255', label:'Everyday Vocab' }, + { x: 1100, y: 2100, color:'255,170,34', label:'Quantity & Time' }, + { x: 2150, y: 1780, color:'255,85,51', label:'Actions & Feelings'}, + { x: 680, y: 1480, color:'102,204,0', label:'ASL Grammar' }, + { x: 2600, y: 1080, color:'180,0,34', label:'Conversation' }, + { x: 1600, y: 310, color:'140,60,255', label:'Fluency' }, +]; + +let currentPortalIdx = 0; + +const viewport = document.getElementById('viewport'); +const world = document.getElementById('world'); +const mmVPBox = document.getElementById('mm-viewport-box'); +const minimap = document.getElementById('minimap'); +const flashOverlay = document.getElementById('flashOverlay'); +const navIndex = document.getElementById('nav-index'); + +let tx = 0, ty = 0, camScale = 1; +let dragging = false, startX, startY, startTx, startTy; +let isFlying = false; +let mapMode = false; + +world.style.transformOrigin = '0 0'; + +function commitTransform(animated, duration) { + const dur = duration !== undefined ? duration : (animated ? 0.82 : 0); + if (animated) { + isFlying = true; + world.style.transition = `transform ${dur}s cubic-bezier(0.4,0,0.2,1)`; + } else { + world.style.transition = 'none'; + } + world.style.transform = `translate(${tx}px,${ty}px) scale(${camScale})`; + updateMinimap(); + if (animated) { + const onEnd = () => { + world.removeEventListener('transitionend', onEnd); + world.style.transition = 'none'; + isFlying = false; + }; + world.addEventListener('transitionend', onEnd); + } +} + +function clampCamera() { + const sw = WORLD_W * camScale, sh = WORLD_H * camScale; + const vw = window.innerWidth, vh = window.innerHeight; + tx = Math.max(sw < vw ? (vw-sw)/2 : -(sw-vw), Math.min(sw < vw ? (vw-sw)/2 : 0, tx)); + ty = Math.max(sh < vh ? (vh-sh)/2 : -(sh-vh), Math.min(sh < vh ? (vh-sh)/2 : 0, ty)); +} + +function flyToPoint(wx, wy, targetScale, animated, duration) { + camScale = targetScale; + tx = window.innerWidth / 2 - wx * camScale; + ty = window.innerHeight / 2 - wy * camScale; + clampCamera(); + commitTransform(animated, duration); +} + +function centerOnPortal(idx, animated) { + mapMode = false; + viewport.classList.remove('map-mode'); + const p = PORTALS[idx]; + flyToPoint(p.x, p.y, 1, animated, 0.82); + currentPortalIdx = idx; + navIndex.textContent = (idx + 1) + ' / ' + PORTALS.length; +} + +function flyToMap() { + mapMode = true; + viewport.classList.add('map-mode'); + const pad = 60; + const targetScale = Math.min( + (window.innerWidth - pad*2) / WORLD_W, + (window.innerHeight - pad*2) / WORLD_H + ); + camScale = targetScale; + tx = (window.innerWidth - WORLD_W * camScale) / 2; + ty = (window.innerHeight - WORLD_H * camScale) / 2; + commitTransform(true, 0.85); + navIndex.textContent = '· / ' + PORTALS.length; +} + +function exitMapToPortal(idx) { + mapMode = false; + viewport.classList.remove('map-mode'); + const p = PORTALS[idx]; + flyToPoint(p.x, p.y, 1, true, 0.85); + currentPortalIdx = idx; + navIndex.textContent = (idx + 1) + ' / ' + PORTALS.length; +} + +function applyTransform(animated) { + clampCamera(); + commitTransform(animated); +} + +function updateMinimap() { + const scaleX = MM_W / WORLD_W, scaleY = MM_H / WORLD_H; + mmVPBox.style.left = (-tx * scaleX) + 'px'; + mmVPBox.style.top = (-ty * scaleY) + 'px'; + mmVPBox.style.width = (window.innerWidth * scaleX) + 'px'; + mmVPBox.style.height = (window.innerHeight * scaleY) + 'px'; +} + +PORTALS.forEach(p => { + const dot = document.createElement('div'); + dot.className = 'mm-dot'; + dot.style.left = (p.x / WORLD_W * MM_W) + 'px'; + dot.style.top = (p.y / WORLD_H * MM_H) + 'px'; + dot.style.background = `rgb(${p.color})`; + dot.style.boxShadow = `0 0 5px rgb(${p.color})`; + minimap.appendChild(dot); +}); + +applyInitialLockState(); +flyToMap(); +updateProgressDots(); + +viewport.addEventListener('mousedown', e => { + if (e.target.closest('.portal-wrap')) return; + if (isFlying) return; + if (mapMode) { + const worldX = (e.clientX - tx) / camScale; + const worldY = (e.clientY - ty) / camScale; + let bestIdx = currentPortalIdx, bestDist = Infinity; + PORTALS.forEach((p, i) => { + const d = Math.hypot(p.x - worldX, p.y - worldY); + if (d < bestDist) { bestDist = d; bestIdx = i; } + }); + exitMapToPortal(bestIdx); + return; + } + dragging = true; + startX = e.clientX; startY = e.clientY; + startTx = tx; startTy = ty; + viewport.classList.add('panning'); +}); +window.addEventListener('mousemove', e => { + if (!dragging) return; + tx = startTx + (e.clientX - startX); + ty = startTy + (e.clientY - startY); + applyTransform(false); +}); +window.addEventListener('mouseup', () => { dragging = false; viewport.classList.remove('panning'); }); + +let touchStartX, touchStartY, touchStartTx, touchStartTy; +viewport.addEventListener('touchstart', e => { + touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; + touchStartTx = tx; touchStartTy = ty; +}, { passive: true }); +viewport.addEventListener('touchmove', e => { + tx = touchStartTx + (e.touches[0].clientX - touchStartX); + ty = touchStartTy + (e.touches[0].clientY - touchStartY); + applyTransform(false); +}, { passive: true }); + +viewport.addEventListener('wheel', e => { + e.preventDefault(); + tx -= e.deltaX; ty -= e.deltaY; + applyTransform(false); +}, { passive: false }); + +window.addEventListener('resize', () => { clampCamera(); commitTransform(false); updateMinimap(); }); + +document.getElementById('btn-home').addEventListener('click', flyToMap); +document.getElementById('btn-current').addEventListener('click', () => centerOnPortal(unlockedCount - 1, true)); +document.getElementById('btn-prev').addEventListener('click', () => centerOnPortal((currentPortalIdx - 1 + PORTALS.length) % PORTALS.length, true)); +document.getElementById('btn-next').addEventListener('click', () => centerOnPortal((currentPortalIdx + 1) % PORTALS.length, true)); + +loadCourseData().then(() => { + if (COURSE_DATA && COURSE_DATA.sections) { + injectWorldDecorations(); + document.querySelectorAll('.portal-wrap').forEach((el, i) => { + const sec = COURSE_DATA.sections[i]; + if (sec) { + el.dataset.theme = sec.theme; + el.dataset.label = sec.label; + } + initPortal(el); + }); + injectCurrentLessonBadge(); + } else { + document.querySelectorAll('.portal-wrap').forEach(el => initPortal(el)); + } +}); + +function injectCurrentLessonBadge() { + const savedSection = localStorage.getItem('asl_current_section'); + const savedVideo = localStorage.getItem('asl_current_video'); + if (!savedSection || !COURSE_DATA) return; + + const secIdx = COURSE_DATA.sections.findIndex(s => s.key === savedSection); + if (secIdx < 0) return; + + const section = COURSE_DATA.sections[secIdx]; + let vidIdx = savedVideo ? section.videos.findIndex(v => v.id === savedVideo) : 0; + if (vidIdx < 0) vidIdx = 0; + + const video = section.videos[vidIdx]; + if (!video) return; + + const wraps = document.querySelectorAll('.portal-wrap'); + const targetWrap = wraps[secIdx]; + if (!targetWrap || targetWrap.classList.contains('locked')) return; + + const badge = document.createElement('div'); + badge.className = 'portal-current-lesson'; + badge.innerHTML = ` +
Current Lesson
+
${video.title}
+ `; + badge.style.setProperty('--pcl-color', section.color); + targetWrap.appendChild(badge); +} + +function injectWorldDecorations() { + const sections = COURSE_DATA.sections; + const positions = PORTALS; + const svg = document.getElementById('path-svg'); + + let svgContent = ''; + + const pathControlPoints = [ + { cx1: 1400, cy1: 2750, cx2: 2200, cy2: 2700 }, + { cx1: 2400, cy1: 2500, cx2: 1400, cy2: 2200 }, + { cx1: 1300, cy1: 1950, cx2: 1900, cy2: 1850 }, + { cx1: 1800, cy1: 1700, cx2: 900, cy2: 1600 }, + { cx1: 800, cy1: 1300, cx2: 2000, cy2: 1150 }, + { cx1: 2400, cy1: 900, cx2: 1900, cy2: 600 }, + ]; + + for (let i = 0; i < positions.length - 1; i++) { + const a = positions[i], b = positions[i + 1]; + const rgb = sections[i].portalColor; + const cp = pathControlPoints[i]; + svgContent += `\n`; + } + + positions.forEach((p, i) => { + const rgb = sections[i].portalColor; + const isTop = i === sections.length - 1; + const op1 = isTop ? 0.24 : 0.22, op2 = isTop ? 0.24 : 0.14; + const sw1 = 1.6, sw2 = isTop ? 1.6 : 1.0; + if (isTop) { + svgContent += ` + +\n`; + } else { + svgContent += ` +\n`; + } + }); + + svg.innerHTML = svgContent; +} + +(function() { + const canvas = document.getElementById('star-canvas'); + const ctx = canvas.getContext('2d'); + let W, H, stars = [], shootingStars = []; + function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; } + window.addEventListener('resize', resize); resize(); + function initStars() { + stars = []; + const count = Math.floor((W * H) / 2200); + for (let i = 0; i < count; i++) { + const tier = Math.random(); + stars.push({ + x: Math.random() * W, y: Math.random() * H, + r: tier > 0.92 ? 1.4 + Math.random() * 0.8 : tier > 0.7 ? 0.8 + Math.random() * 0.5 : 0.3 + Math.random() * 0.4, + baseAlpha: 0.15 + Math.random() * 0.75, + twinkleSpeed: 0.5 + Math.random() * 2.5, + twinkleOffset: Math.random() * Math.PI * 2, + color: (() => { const r = Math.random(); return r>0.93?[180,200,255]:r>0.87?[255,240,200]:r>0.82?[200,160,255]:[220,220,240]; })(), + }); + } + } + initStars(); window.addEventListener('resize', initStars); + function spawnShooting() { + if (shootingStars.length < 3 && Math.random() < 0.004) { + const angle = -0.3 + Math.random() * 0.4; + shootingStars.push({ x: Math.random()*W, y: Math.random()*H*0.5, + vx: Math.cos(angle)*(4+Math.random()*6), vy: Math.sin(angle)*(4+Math.random()*6)+1.5, + len: 60+Math.random()*100, alpha: 0.8+Math.random()*0.2, life: 1.0 }); + } + } + let frame = 0; + function draw() { + ctx.clearRect(0,0,W,H); + const t = frame * 0.016; + stars.forEach(s => { + const tw = 0.5 + 0.5 * Math.sin(t * s.twinkleSpeed + s.twinkleOffset); + const al = s.baseAlpha * (0.45 + 0.55 * tw); + const [r,g,b] = s.color; + ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); + ctx.fillStyle = `rgba(${r},${g},${b},${al.toFixed(3)})`; ctx.fill(); + if (s.r > 1.0 && al > 0.5) { + const grd = ctx.createRadialGradient(s.x,s.y,0,s.x,s.y,s.r*3.5); + grd.addColorStop(0,`rgba(${r},${g},${b},${(al*0.3).toFixed(3)})`); + grd.addColorStop(1,`rgba(${r},${g},${b},0)`); + ctx.beginPath(); ctx.arc(s.x,s.y,s.r*3.5,0,Math.PI*2); ctx.fillStyle=grd; ctx.fill(); + } + }); + spawnShooting(); + shootingStars = shootingStars.filter(s=>s.life>0.01); + shootingStars.forEach(s => { + s.x+=s.vx; s.y+=s.vy; s.life*=0.94; + const g=ctx.createLinearGradient(s.x-s.vx*s.len/6,s.y-s.vy*s.len/6,s.x,s.y); + g.addColorStop(0,`rgba(255,255,255,0)`); + g.addColorStop(1,`rgba(255,255,255,${(s.alpha*s.life).toFixed(3)})`); + ctx.beginPath(); ctx.moveTo(s.x-s.vx*s.len/6,s.y-s.vy*s.len/6); ctx.lineTo(s.x,s.y); + ctx.strokeStyle=g; ctx.lineWidth=1.5; ctx.stroke(); + }); + frame++; requestAnimationFrame(draw); + } + draw(); + })(); + + (function() { + const layer = document.getElementById('ambient-layer'); + const blobs = [ + {color:'60,20,120',size:600,x:10,y:5,dur:38,op:[0.045,0.085],dx:['30px','-20px'],dy:['20px','40px']}, + {color:'0,40,120',size:500,x:75,y:15,dur:52,op:[0.035,0.065],dx:['-40px','15px'],dy:['30px','-25px']}, + {color:'80,10,60',size:450,x:45,y:60,dur:45,op:[0.04,0.07],dx:['20px','-35px'],dy:['-30px','20px']}, + {color:'20,80,60',size:400,x:20,y:80,dur:60,op:[0.025,0.055],dx:['35px','-10px'],dy:['-20px','30px']}, + {color:'100,30,20',size:480,x:80,y:70,dur:42,op:[0.03,0.06],dx:['-25px','30px'],dy:['25px','-40px']}, + {color:'40,60,140',size:550,x:55,y:35,dur:56,op:[0.03,0.055],dx:['15px','-30px'],dy:['-35px','15px']}, + ]; + blobs.forEach((b,i) => { + const el = document.createElement('div'); el.className = 'nebula-blob'; + el.style.cssText = `width:${b.size}px;height:${b.size*0.7}px;left:${b.x}%;top:${b.y}%; + background:radial-gradient(ellipse at center,rgba(${b.color},0.18) 0%,rgba(${b.color},0.06) 50%,transparent 75%); + --nb-op-lo:${b.op[0]};--nb-op-hi:${b.op[1]};--dx1:${b.dx[0]};--dy1:${b.dy[0]};--dx2:${b.dx[1]};--dy2:${b.dy[1]}; + animation-duration:${b.dur}s;animation-delay:${-i*7.3}s;opacity:${b.op[0]};`; + layer.appendChild(el); + }); + })(); + + (function() { + const colors=['140,60,255','34,221,136','68,153,255','255,170,34','255,85,51','102,204,0']; + for (let i=0;i<28;i++) { + const el=document.createElement('div'); el.className='dust-mote'; + const size=1+Math.random()*2.5, color=colors[Math.floor(Math.random()*colors.length)]; + const op=0.1+Math.random()*0.3, dur=12+Math.random()*24; + el.style.cssText=`width:${size}px;height:${size}px;left:${Math.random()*100}%;top:${20+Math.random()*70}%; + background:rgba(${color},0.8);box-shadow:0 0 ${size*3}px rgba(${color},0.6); + --mote-op:${op};--mote-dx:${(Math.random()-0.5)*80}px;--mote-dy:${-40-Math.random()*120}px; + animation-duration:${dur}s;animation-delay:${-Math.random()*dur}s;filter:blur(${size>2?0.5:0}px);`; + document.body.appendChild(el); + } + })(); \ No newline at end of file diff --git a/projecthearthstone.in/Courses/ASL course/style.css b/projecthearthstone.in/Courses/ASL course/style.css new file mode 100644 index 0000000..a1ddd45 --- /dev/null +++ b/projecthearthstone.in/Courses/ASL course/style.css @@ -0,0 +1,364 @@ +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } + +html, body { + width: 100%; height: 100%; + background: #06060b; + overflow: hidden; +} + +#viewport { top: 0 !important; } + #progress-bar { top: 50%; } + #nav-cluster { bottom: 22px; } + #minimap { bottom: 22px; } + +.flash-overlay { + position: fixed; inset: 0; + pointer-events: none; opacity: 0; z-index: 999; +} +.flash-overlay.flash { animation: flashOut 0.45s ease-out forwards; } +@keyframes flashOut { 0% { opacity: 0.28; } 100% { opacity: 0; } } + +#viewport { + position: fixed; inset: 0; + overflow: hidden; + cursor: grab; + user-select: none; -webkit-user-select: none; + z-index: 2; +} +#viewport.panning { cursor: grabbing; } + +#world { + position: absolute; + width: 3200px; height: 3200px; + top: 0; left: 0; + will-change: transform; +} + +#tree-img { + position: absolute; + width: 3100px; height: auto; + left: 50px; top: 0px; + pointer-events: none; + user-select: none; -webkit-user-drag: none; + filter: brightness(0.80) saturate(0.88); +} + +#path-svg { + position: absolute; top: 0; left: 0; + width: 3200px; height: 3200px; + pointer-events: none; overflow: visible; +} + +.portal-aura { + position: absolute; + transform: translate(-50%, -50%); + width: 260px; height: 260px; + border-radius: 50%; + pointer-events: none; + z-index: 1; +} +.portal-aura.boss-aura { width: 360px; height: 360px; } + +.portal-wrap { + position: absolute; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + z-index: 10; +} + +.portal-wrap.boss .portal-svg { + width: 280px !important; + height: 280px !important; +} + +.portal-svg { + cursor: pointer; + overflow: visible; + will-change: transform, filter; + display: block; +} +.portal-wrap.locked .portal-svg { + cursor: default; + filter: none !important; + transform: none !important; +} + +.portal-label { + font-size: 9px; + letter-spacing: 0.35em; + text-transform: uppercase; + color: rgba(255,255,255,0.30); + transition: color 0.4s, letter-spacing 0.4s; + pointer-events: none; + white-space: nowrap; +} +.portal-wrap.hovered .portal-label { + color: rgba(255,255,255,0.75); + letter-spacing: 0.42em; +} + +.portal-theme-tag { display: none; } + +.portal-lock-icon { + transition: transform 0.35s cubic-bezier(0.34,1.6,0.64,1), opacity 0.3s; + transform-origin: 120px 120px; + pointer-events: none; +} +.portal-wrap.locked:hover .portal-lock-icon { + transform: scale(1.18); + opacity: 1 !important; +} + +#hud-title { + position: fixed; + top: 20px; left: 50%; + transform: translateX(-50%); + font-size: 10px; + letter-spacing: 0.55em; + text-transform: uppercase; + color: rgba(255,255,255,0.12); + pointer-events: none; + z-index: 200; + white-space: nowrap; + text-shadow: 0 0 20px rgba(140,60,255,0.3), 0 0 40px rgba(140,60,255,0.12); + animation: titleBreath 6s ease-in-out infinite; +} +@keyframes titleBreath { + 0%, 100% { opacity: 0.12; } + 50% { opacity: 0.22; } +} + +#progress-bar { + position: fixed; + right: 20px; top: 50%; + transform: translateY(-50%); + display: flex; flex-direction: column; gap: 9px; + z-index: 200; pointer-events: none; +} +.pdot { + width: 5px; height: 5px; border-radius: 50%; + background: rgba(255,255,255,0.08); + border: 0.5px solid rgba(255,255,255,0.18); + transition: background 0.35s, box-shadow 0.35s; +} +.pdot.on { + background: rgba(255,255,255,0.55); + animation: dotPulse 3s ease-in-out infinite; +} +@keyframes dotPulse { + 0%, 100% { box-shadow: 0 0 5px rgba(255,255,255,0.25); } + 50% { box-shadow: 0 0 10px rgba(255,255,255,0.55); } +} + +#nav-cluster { + position: fixed; + bottom: 22px; left: 20px; + display: flex; flex-direction: column; gap: 8px; + z-index: 200; +} + +.nav-row { display: flex; gap: 6px; align-items: center; } + +.nav-btn { + display: flex; align-items: center; gap: 6px; + padding: 0 16px; + height: 38px; + background: rgba(255,255,255,0.10); + border: 1px solid rgba(255,255,255,0.28); + border-radius: 8px; + color: rgba(255,255,255,0.75); + font-size: 9px; + letter-spacing: 0.28em; + text-transform: uppercase; + cursor: pointer; + transition: background 0.2s, color 0.2s, border-color 0.2s, transform 0.12s; + white-space: nowrap; + outline: none; +} +.nav-btn:hover { + background: rgba(255,255,255,0.18); + border-color: rgba(255,255,255,0.55); + color: rgba(255,255,255,1); + box-shadow: 0 0 12px rgba(255,255,255,0.08); +} +.nav-btn:active { transform: scale(0.96); } + +.nav-btn.accent { + border-color: rgba(140,60,255,0.4); + color: rgba(180,130,255,0.7); +} +.nav-btn.accent:hover { + background: rgba(140,60,255,0.12); + border-color: rgba(180,130,255,0.6); + color: rgba(210,175,255,0.95); +} + +.nav-btn.nav-arrow { + padding: 0 12px; + min-width: 70px; + justify-content: center; +} + +.nav-index { + font-size: 10px; + letter-spacing: 0.2em; + color: rgba(255,255,255,0.55); + min-width: 36px; + text-align: center; + pointer-events: none; +} + +#minimap { + position: fixed; + bottom: 22px; right: 20px; + width: 88px; height: 88px; + background: rgba(0,0,0,0.55); + border: 0.5px solid rgba(255,255,255,0.1); + border-radius: 6px; + overflow: hidden; + z-index: 200; + pointer-events: none; +} +#mm-viewport-box { + position: absolute; + border: 1px solid rgba(255,255,255,0.35); + border-radius: 2px; + background: rgba(255,255,255,0.04); + pointer-events: none; +} +.mm-dot { + position: absolute; + width: 4px; height: 4px; + border-radius: 50%; + transform: translate(-50%,-50%); +} + +#world.flying { + transition: transform 0.75s cubic-bezier(0.4, 0, 0.2, 1); +} + +#viewport.overview-mode { cursor: default; } +#viewport.overview-mode #world { + transition: transform 0.9s cubic-bezier(0.4, 0, 0.2, 1) !important; +} +#world.overview-scaled { + transform-origin: top left; + transition: transform 0.9s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +#star-canvas { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + opacity: 0.85; +} + +#ambient-layer { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + overflow: hidden; +} + +.nebula-blob { + position: absolute; + border-radius: 50%; + filter: blur(80px); + mix-blend-mode: screen; + animation: nebulaFloat linear infinite; + will-change: transform, opacity; +} +@keyframes nebulaFloat { + 0% { transform: translate(0, 0) scale(1); opacity: var(--nb-op-lo); } + 33% { transform: translate(var(--dx1), var(--dy1)) scale(1.08); opacity: var(--nb-op-hi); } + 66% { transform: translate(var(--dx2), var(--dy2)) scale(0.96); opacity: var(--nb-op-lo); } + 100% { transform: translate(0, 0) scale(1); opacity: var(--nb-op-lo); } +} + +.dust-mote { + position: fixed; + border-radius: 50%; + pointer-events: none; + z-index: 1; + animation: dustFloat linear infinite; + will-change: transform, opacity; +} +@keyframes dustFloat { + 0% { transform: translateY(0px) translateX(0px); opacity: 0; } + 10% { opacity: var(--mote-op); } + 90% { opacity: var(--mote-op); } + 100% { transform: translateY(var(--mote-dy)) translateX(var(--mote-dx)); opacity: 0; } +} + +#vignette { + position: fixed; + inset: 0; + background: radial-gradient(ellipse at 50% 50%, + transparent 30%, + rgba(0,0,0,0.25) 65%, + rgba(0,0,0,0.65) 100%); + pointer-events: none; + z-index: 190; +} + +#scanlines { + position: fixed; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0,0,0,0.04) 2px, + rgba(0,0,0,0.04) 4px + ); + pointer-events: none; + z-index: 191; +} + +#hud-title, #progress-bar, #nav-cluster, #minimap { z-index: 200; } + +@keyframes unlockPulse { + 0% { box-shadow: 0 0 0 0 rgba(255,255,255,0.4); } + 70% { box-shadow: 0 0 0 40px rgba(255,255,255,0); } + 100% { box-shadow: 0 0 0 0 rgba(255,255,255,0); } +} + +.portal-current-lesson { + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + padding: 7px 14px; + background: rgba(6,6,11,0.82); + border: 1px solid var(--pcl-color, rgba(255,255,255,0.2)); + border-radius: 8px; + pointer-events: none; + white-space: nowrap; + box-shadow: 0 0 18px color-mix(in srgb, var(--pcl-color, white) 30%, transparent); + animation: pclFadeIn 0.5s ease both; +} +@keyframes pclFadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} +.pcl-eyebrow { + font-size: 7px; + letter-spacing: 0.32em; + text-transform: uppercase; + color: var(--pcl-color, rgba(255,255,255,0.4)); + opacity: 0.65; +} +.pcl-title { + font-size: 9px; + letter-spacing: 0.12em; + color: rgba(255,255,255,0.82); + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; +} \ No newline at end of file diff --git a/projecthearthstone.in/login/get_database.js b/projecthearthstone.in/login/get_database.js new file mode 100644 index 0000000..e69de29 diff --git a/projecthearthstone.in/login/login.html b/projecthearthstone.in/login/login.html new file mode 100644 index 0000000..e69de29