1document.addEventListener('DOMContentLoaded', () => { 2 const iconsContainer = document.querySelector('.social-icons-container'); 3 const radialSection = document.querySelector('.radial-section'); 4 5 // We adjust the radius dynamically to support smaller mobile screens. 6 // We adjust the radius dynamically to support smaller mobile screens. 7 let radius = getRadius(); 8 let centerX = radialSection.clientWidth / 2; 9 let centerY = radialSection.clientHeight / 2; 10 11 // --- Rotation and Interactivity State --- 12 let currentRotation = 0; // Total rotation in radians 13 let isDragging = false; 14 let startAngle = 0; 15 let lastRotation = 0; 16 let velocity = 0; 17 let lastUpdateTime = 0; 18 let animationId = null; 19 20 function getRadius() { 21 // Calculate max allowed radius based on screen width with padding to avoid overflow 22 return Math.min(150, (radialSection.clientWidth / 2) - 40); 23 } 24 25 const icons = [ 26 { class: 'fa-solid fa-briefcase', styleClass: 'portfolio', href: 'https://uncledrewdev.vercel.app/' }, 27 { class: 'fa-brands fa-linkedin-in', styleClass: 'linkedin', href: 'https://www.linkedin.com/in/uncledrewdev' }, 28 { class: 'fa-brands fa-dev', styleClass: 'devto', href: 'https://dev.to/uncle_drew' }, 29 { class: 'fa-solid fa-code-branch', styleClass: 'builds', id: 'builds-btn', href: '#' }, 30 // { class: 'fa-brands fa-x-twitter', styleClass: 'twitter', href: 'https://x.com/UncleDrewDev' }, // Updated to X icon 31 { class: 'fa-solid fa-wallet', styleClass: 'wallet', id: 'support-btn', href: 'https://selar.com/showlove/uncledrewdev' }, 32 { class: 'fa-brands fa-github', styleClass: 'github', href: 'https://github.com/Tremy-t' }, 33 { class: 'fas fa-shopping-bag', styleClass: 'store', href: 'https://selar.com/m/UDrew' } 34 ]; 35 36 // Add the connecting ring 37 const ring = document.createElement('div'); 38 ring.className = 'ring'; 39 radialSection.appendChild(ring); 40 41 const numIcons = icons.length; 42 // We want to offset so an icon or gap aligns nicely. 43 // Let's start the first icon near the top. offset by -PI/2. 44 const baseAngleOffset = -Math.PI / 2; 45 46 // Create elements 47 const nodes = []; 48 const lines = []; 49 50 icons.forEach((icon, index) => { 51 // Create icon node 52 const node = document.createElement('a'); 53 node.href = icon.href || '#'; 54 if (icon.href !== '#') { 55 node.target = '_blank'; 56 node.rel = 'noopener noreferrer'; 57 } else { 58 node.addEventListener('click', (e) => e.preventDefault()); 59 } 60 node.className = `social-node ${icon.styleClass}`; 61 if (icon.id) { 62 node.id = icon.id; 63 } 64 65 if (icon.src) { 66 const img = document.createElement('img'); 67 img.src = icon.src; 68 img.alt = icon.styleClass; 69 img.className = 'custom-icon'; // styling hook 70 node.appendChild(img); 71 } else { 72 const i = document.createElement('i'); 73 i.className = icon.class; 74 node.appendChild(i); 75 } 76 77 iconsContainer.appendChild(node); 78 nodes.push({ element: node, baseAngle: (index * (2 * Math.PI) / numIcons) }); 79 80 // Draw connection line 81 const line = document.createElement('div'); 82 line.className = 'line'; 83 radialSection.appendChild(line); 84 lines.push(line); 85 }); 86 87 // Positioning function to be called on load, resize, and during rotation 88 function setupPositions() { 89 radius = getRadius(); 90 centerX = radialSection.clientWidth / 2; 91 centerY = radialSection.clientHeight / 2; 92 93 // Resize the inner ring continuously 94 ring.style.width = `${radius * 2}px`; 95 ring.style.height = `${radius * 2}px`; 96 97 nodes.forEach((nodeObj, index) => { 98 const angle = baseAngleOffset + nodeObj.baseAngle + currentRotation; 99 const x = (centerX + radius * Math.cos(angle)).toFixed(2); 100 const y = (centerY + radius * Math.sin(angle)).toFixed(2); 101 nodeObj.element.style.left = `${x}px`; 102 nodeObj.element.style.top = `${y}px`; 103
104 // Adjust line width to new radius and rotation 105 if (lines[index]) { 106 lines[index].style.width = `${radius}px`; 107 const degrees = (angle * (180 / Math.PI)).toFixed(2); 108 lines[index].style.transform = `rotate(${degrees}deg)`; 109 } 110 }); 111 } 112 113 // --- Interaction Handlers --- 114 115 // Scroll wheel rotation 116 radialSection.addEventListener('wheel', (e) => { 117 e.preventDefault(); 118 const delta = e.deltaY || e.deltaX; 119 currentRotation += delta * 0.005; 120 velocity = delta * 0.002; // Simple momentum 121 cancelAnimationFrame(animationId); 122 setupPositions(); 123 applyInertia(); 124 }, { passive: false }); 125 126 // Helper to get angle from center to mouse/touch point 127 function getAngle(x, y) { 128 const rect = radialSection.getBoundingClientRect(); 129 const centerX = rect.left + rect.width / 2; 130 const centerY = rect.top + rect.height / 2; 131 return Math.atan2(y - centerY, x - centerX); 132 } 133 134 function handleStart(e) { 135 const point = e.touches ? e.touches[0] : e; 136 isDragging = true; 137 startAngle = getAngle(point.clientX, point.clientY) - currentRotation; 138 lastRotation = currentRotation; 139 lastUpdateTime = performance.now(); 140 velocity = 0; 141 cancelAnimationFrame(animationId); 142 radialSection.style.cursor = 'grabbing'; 143 } 144 145 function handleMove(e) { 146 if (!isDragging) return; 147 const point = e.touches ? e.touches[0] : e; 148 const newAngle = getAngle(point.clientX, point.clientY); 149 const newRotation = newAngle - startAngle; 150 151 const now = performance.now(); 152 const dt = now - lastUpdateTime; 153 if (dt > 0) { 154 velocity = (newRotation - currentRotation) / dt * 16; // Velocity per frame (~16ms) 155 } 156 157 currentRotation = newRotation; 158 lastUpdateTime = now; 159 setupPositions(); 160 } 161 162 function handleEnd() { 163 if (!isDragging) return; 164 isDragging = false; 165 radialSection.style.cursor = 'grab'; 166 applyInertia(); 167 } 168 169 function applyInertia() { 170 if (Math.abs(velocity) < 0.001) return; 171 172 currentRotation += velocity; 173 velocity *= 0.95; // Friction 174 setupPositions(); 175 animationId = requestAnimationFrame(applyInertia); 176 } 177 178 radialSection.style.cursor = 'grab'; 179 radialSection.addEventListener('mousedown', handleStart); 180 window.addEventListener('mousemove', handleMove); 181 window.addEventListener('mouseup', handleEnd); 182 183 radialSection.addEventListener('touchstart', handleStart, { passive: false }); 184 window.addEventListener('touchmove', handleMove, { passive: false }); 185 window.addEventListener('touchend', handleEnd); 186 187 // Initial position setup 188 setupPositions(); 189 190 // --- Navigation Logic --- 191 const navItems = document.querySelectorAll('.nav-item'); 192 const views = document.querySelectorAll('.view'); 193 194 function switchView(viewId) { 195 // Update Nav UI 196 navItems.forEach(item => { 197 if (item.getAttribute('data-view') === viewId) { 198 item.classList.add('active'); 199 } else { 200 item.classList.remove('active'); 201 } 202 }); 203 204 // Update Views 205 views.forEach(view => { 206 if (view.id === `${viewId}-view`) { 207 view.classList.add('active'); 208 } else { 209 view.classList.remove('active'); 210 } 211 }); 212 213 // Special handling for Home View (Radial Section) 214 if (viewId === 'home') { 215 // Recalculate positions to ensure everything is aligned 216 setTimeout(setupPositions, 10); 217 } 218 } 219 220 navItems.forEach(item => { 221 item.addEventListener('click', () => { 222 const viewId = item.getAttribute('data-view'); 223 224 // External Redirection for specific views 225 if (viewId === 'store') { 226 window.open('https://selar.com/m/UDrew', '_blank'); 227 return; 228 } 229 if (viewId === 'prompts') { 230 // window.open('https://androdojo.vercel.app/', '_blank'); 231 return; 232 } 233 if (viewId === 'portfolio') { 234 window.open('https://uncledrewdev.vercel.app/', '_blank'); 235 return; 236 } 237 238 switchView(viewId); 239 }); 240 }); 241 242 243 // Handle resizing 244 window.addEventListener('resize', () => { 245 if (document.getElementById('home-view').classList.contains('active')) { 246 setupPositions(); 247 } 248 }); 249 250 251 // Modal Logic Helper 252 function setupModal(btnId, modalId, closeId) { 253 const btn = document.getElementById(btnId); 254 const modal = document.getElementById(modalId); 255 const closeBtn = document.getElementById(closeId); 256 257 if (btn && modal && closeBtn) { 258 btn.addEventListener('click', (e) => { 259 e.stopPropagation(); 260 modal.classList.add('active'); 261 document.body.style.overflow = 'hidden'; 262 }); 263 264 closeBtn.addEventListener('click', () => { 265 modal.classList.remove('active'); 266 if (!document.querySelector('.modal-overlay.active')) { 267 document.body.style.overflow = ''; 268 } 269 }); 270 271 modal.addEventListener('click', (e) => { 272 if (e.target === modal) { 273 modal.classList.remove('active'); 274 if (!document.querySelector('.modal-overlay.active')) { 275 document.body.style.overflow = ''; 276 } 277 } 278 }); 279 } 280 } 281 282 setupModal('profile-container', 'profile-modal', 'profile-modal-close'); 283 284 // Add listener for builds radial button 285 const buildsBtn = document.getElementById('builds-btn'); 286 if (buildsBtn) { 287 buildsBtn.addEventListener('click', (e) => { 288 e.preventDefault(); 289 switchView('builds'); 290 }); 291 } 292 293 // Universal Close on Escape key 294 document.addEventListener('keydown', (e) => { 295 if (e.key === 'Escape') { 296 const activeModal = document.querySelector('.modal-overlay.active'); 297 if (activeModal) { 298 activeModal.classList.remove('active'); 299 document.body.style.overflow = ''; 300 } 301 } 302 }); 303}); 304 305 306setupModal('profile-container', 'profile-modal', 'profile-modal-close'); 307// Removed builds-btn modal setup since it's now a view 308 309// Universal Close on Escape key 310document.addEventListener('keydown', (e) => { 311 if (e.key === 'Escape') { 312 const activeModal = document.querySelector('.modal-overlay.active'); 313 if (activeModal) { 314 activeModal.classList.remove('active'); 315 document.body.style.overflow = ''; 316 } 317 } 318}); 319
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.