1/** 2 * Dragon Cursor Follower 3 * SVG-based animated dragon that follows the cursor 4 */ 5 6(function() { 7 'use strict'; 8 9 const xmlns = "http://www.w3.org/2000/svg"; 10 const xlinkns = "http://www.w3.org/1999/xlink"; 11 12 // Module state 13 let screen = null; 14 let svgContainer = null; 15 let animationId = null; 16 let isInitialized = false; 17 let width, height; 18 let elems = []; 19 let pointer = { x: 0, y: 0 }; 20 let radm = 0; 21 let frm = Math.random(); 22 let rad = 0; 23 24 // Configuration constants 25 const MIN_SEGMENTS = 15; 26 let maxSegments = 35; // Maximum number of segments allowed (can be changed at runtime) 27 const DEFAULT_SEGMENT_RANGE = 20; // Default range for random generation 28 29 // Procedural generation parameters 30 let dragonParams = { 31 segmentCount: 40, 32 finPositions: [8, 14], 33 headColor: '#ffffff', 34 headOutlineColor: '#000000', 35 finGradientStart: '#cccccc', 36 finGradientEnd: '#000000', 37 spineGradientStart: '#cccccc', 38 spineGradientEnd: '#333333', 39 scale: 0.3, 40 segmentSpacing: 5, 41 animationSpeed: 0.003 42 }; 43 44 // Generate random dragon parameters 45 const generateDragonParams = () => { 46 // Generate segment count within min/max range, capped at maxSegments 47 const maxRange = Math.min(maxSegments - MIN_SEGMENTS, DEFAULT_SEGMENT_RANGE); 48 const segmentCount = Math.min( 49 maxSegments, 50 Math.floor(MIN_SEGMENTS + Math.random() * maxRange) 51 ); // MIN_SEGMENTS to maxSegments segments 52 53 // Generate 2-4 random fin positions (not at head or tail) 54 const finCount = Math.floor(2 + Math.random() * 3); // 2-4 fins 55 const finPositions = []; 56 for (let i = 0; i < finCount; i++) { 57 let pos; 58 do { 59 pos = Math.floor(2 + Math.random() * (segmentCount - 3)); 60 } while (finPositions.includes(pos)); // Ensure unique positions 61 finPositions.push(pos); 62 } 63 finPositions.sort((a, b) => a - b); 64 65 // Random colors - head can be various colors 66 const headColors = ['#ffffff', '#ffcccc', '#ccffcc', '#ccccff', '#ffffcc', '#ffccff']; 67 const headColor = headColors[Math.floor(Math.random() * headColors.length)]; 68 69 // Random gradient colors for fins and spine 70 const generateGradient = () => { 71 const colors = [ 72 { start: '#cccccc', end: '#000000' },
73 { start: '#dddddd', end: '#333333' }, 74 { start: '#bbbbbb', end: '#222222' }, 75 { start: '#ccccff', end: '#000033' }, 76 { start: '#ffcccc', end: '#330000' }, 77 { start: '#ccffcc', end: '#003300' } 78 ]; 79 return colors[Math.floor(Math.random() * colors.length)]; 80 }; 81 82 const finGradient = generateGradient(); 83 const spineGradient = generateGradient(); 84 85 // Scale variation (0.15 to 0.45) - more variation between generations 86 const scale = 0.15 + Math.random() * 0.3; 87 88 // Segment spacing variation (4 to 6) 89 const segmentSpacing = 4 + Math.random() * 2; 90 91 // Animation speed variation 92 const animationSpeed = 0.002 + Math.random() * 0.002; 93 94 return { 95 segmentCount, 96 finPositions, 97 headColor, 98 headOutlineColor: '#000000', 99 finGradientStart: finGradient.start, 100 finGradientEnd: finGradient.end, 101 spineGradientStart: spineGradient.start, 102 spineGradientEnd: spineGradient.end, 103 scale, 104 segmentSpacing, 105 animationSpeed 106 }; 107 }; 108 109 const resize = () => { 110 width = window.innerWidth; 111 height = window.innerHeight; 112 if (isInitialized && pointer.x !== undefined && pointer.y !== undefined) { 113 radm = Math.min(pointer.x, pointer.y) - 20; 114 } 115 }; 116 117 const prepend = (use, i) => { 118 if (!screen) { 119 console.error('ð Dragon: screen element not found in prepend'); 120 return; 121 } 122 const elem = document.createElementNS(xmlns, "use"); 123 elems[i].use = elem; 124 elem.setAttributeNS(xlinkns, "xlink:href", "#" + use); 125 screen.prepend(elem); 126 // Element created silently 127 }; 128 129 const init = () => { 130 if (isInitialized) { 131 console.warn('Dragon follower already initialized'); 132 return; 133 } 134 135 // Find or create SVG container 136 svgContainer = document.getElementById('dragon-follower'); 137 if (!svgContainer) { 138 // Create SVG container if it doesn't exist 139 svgContainer = document.createElementNS(xmlns, 'svg'); 140 svgContainer.id = 'dragon-follower'; 141 // Set viewBox to match screen dimensions for pixel-perfect coordinates 142 svgContainer.setAttribute('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`); 143 svgContainer.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999; overflow: visible;'; 144 145 // Add defs with SVG definitions 146 const defs = document.createElementNS(xmlns, 'defs'); 147 148 // Cabeza (head) - use procedural colors 149 const cabeza = document.createElementNS(xmlns, 'g'); 150 cabeza.id = 'Cabeza'; 151 cabeza.innerHTML = ` 152 <path style="fill: ${dragonParams.headColor}; fill-opacity: 1" d="M-28.9,-1.1L-28.55 -1.95Q-28.1 -3.1 -27.25 -2.95L-26.7 -2.95Q-27.7 -1.65 -28.9 -1.1M-18.35,-1.8Q-15.1 -10.3 -9.6 -6.05Q-15.1 -6.2 -18.35 -1.8M-18.35,1.1Q-15.1 5.45 -9.6 5.35Q-15.1 9.55 -18.35 1.1M-26.7,2.2L-27.25 2.25Q-28.1 2.4 -28.55 1.2L-28.9 0.35Q-27.7 0.9 -26.7 2.2"/> 153 <path style="fill: ${dragonParams.headOutlineColor}; fill-opacity: 1" d="M-21.05,-8.25Q-13.6 -15.95 -1.3 -12.1Q-7.85 -8.5 -5.85 -4.35Q-2.3 -4.85 10.5 0.15Q0 4.35 -5.85 3.65Q-7.85 7.75 -1.25 12.45Q-13.6 15.2 -21.05 7.5Q-29.55 4.05 -30.2 -0.35Q-29.55 -4.8 -21.05 -8.25M-26.7,-2.95L-27.25 -2.95Q-28.1 -3.1 -28.55 -1.95L-28.9 -1.1Q-27.7 -1.65 -26.7 -2.95M-9.6,-6.05Q-15.1 -10.3 -18.35 -1.8Q-15.1 -6.2 -9.6 -6.05M-9.6,5.35Q-15.1 5.45 -18.35 1.1Q-15.1 9.55 -9.6 5.35M-28.9,0.35L-28.55 1.2Q-28.1 2.4 -27.25 2.25L-26.7 2.2Q-27.7 0.9 -28.9 0.35"/> 154 `; 155 156 // Aletas (fins) 157 const aletas = document.createElementNS(xmlns, 'g'); 158 aletas.id = 'Aletas'; 159 const aletasGradient = document.createElementNS(xmlns, 'linearGradient'); 160 aletasGradient.id = 'LinearGradID_1'; 161 aletasGradient.setAttribute('gradientUnits', 'userSpaceOnUse'); 162 aletasGradient.setAttribute('x1', '-819.2'); 163 aletasGradient.setAttribute('y1', '0'); 164 aletasGradient.setAttribute('x2', '819.2'); 165 aletasGradient.setAttribute('y2', '0'); 166 aletasGradient.innerHTML = ` 167 <stop offset="0" style="stop-color: ${dragonParams.finGradientStart}; stop-opacity: 1"/> 168 <stop offset="1" style="stop-color: ${dragonParams.finGradientEnd}; stop-opacity: 1"/> 169 `; 170 aletas.innerHTML = ` 171 <path style="fill: url(#LinearGradID_1)" d="M29.75,-36.85Q-17.75 -61.45 -42.05 -40.95L-45.35 -38.35L-53.7 -41.15L-51.15 -44.85Q-34.85 -68.4 21 -57.8Q-32.2 -72.1 -50.25 -50Q-53.85 -45.65 -56.05 -41.95L-64.7 -43.35L-60.6 -50.3Q-45.9 -75.55 5.1 -79.35Q-2.2 -79.8 -9.45 -79.15Q-16.2 -78.55 -22.85 -77.15Q-29.85 -75.65 -36.5 -73Q-43.05 -70.4 -48.8 -66.85Q-54.55 -63.35 -56.8 -60.3L-60.5 -55.4Q-62.95 -52.1 -67 -43.55L-70.55 -43.55L-76.35 -42.95Q-74.6 -49.1 -71.85 -54.85Q-68.9 -61.25 -64.8 -67.1Q-60.8 -73 -55.45 -77.55Q-49.9 -82.35 -43.65 -85.85L-30.6 -92.7Q-24.05 -95.95 -17 -98.25Q-63.75 -86.35 -73.65 -57.1Q-75.75 -50.75
171-77.45 -42.75Q-82.9 -41.75 -88 -39.65Q-87.65 -46.65 -86.3 -53.05Q-79.8 -89.8 -36.65 -117.2Q-80.65 -94.5 -87.55 -59.55Q-88.65 -54.15 -88.95 -39.4L-89.8 -38.85L-92.7 -37.6Q-93.75 -44.35 -94.1 -51.15Q-94.4 -58.2 -93.25 -65.1Q-92.15 -72.5 -90.05 -79.65Q-88.05 -86.55 -85 -93Q-82.1 -99.3 -78.45 -105.15Q-74.6 -111.35 -70.25 -117.25Q-65.95 -123.1 -61.1 -128.55Q-70.3 -119.35 -77.9 -108.7Q-86 -97.3 -90.8 -84.05Q-95.8 -70.5 -96 -56.15Q-96.1 -46 -94.05 -36.05L-93.25 -31.55Q-93.5 -35.65 -92.35 -36Q-79.85 -42 -66.6 -40.45Q-52.45 -38.85 -39.2 -33.25Q-28.3 -29.9 -21.25 -24.15Q-17.8 -23.3 -8.6 -15.6Q-12.1 -20.75 -16.75 -24.5Q-24.55 -30.7 -34.25 -34.05L-42.55 -37Q-38.9 -41.25 -31.5 -43.25Q-24.05 -45.3 -16.2 -46.3Q-8.35 -47.35 -1 -46Q5.95 -44.75 12.75 -42.85Q19.85 -40.9 29.75 -36.85M-92.45,-27.35L-94.95 -36.25Q-109.7 -105 -27.95 -154.65Q-98.65 -103.8 -91.75 -39.4L-89.95 -40.2Q-92.2 -105.25 -5.6 -130.9Q-78.8 -99.95 -87.45 -40.9Q-83.15 -42.95 -78.45 -43.95Q-70 -101.3 17.65 -103.8Q-56.9 -93.4 -74.5 -44.55L-67.4 -45.45Q-49.1 -94.95 39.25 -75.65Q-36.75 -84.35 -62.25 -44.25L-57.3 -43.6Q-31.65 -86.5 56.15 -46.05Q-20.3 -73.35 -51.35 -41.7L-45.95 -39.75Q-17.85 -71.35 51.85 -24.8Q-8.7 -56.4 -39.75 -37.05Q-28.15 -34.05 -14.25 -24.45Q-8.6 -19.85 -5.8 -16.95Q5.95 -2.4 20 0Q5.95 2.4 -5.8 16.95Q-8.6 19.85 -14.25 24.45Q-28.15 34.05 -39.75 37.05Q-8.7 56.4 51.85 24.8Q-17.85 71.35 -45.95 39.75L-51.35 41.7Q-20.3 73.35 56.15 46.1Q-31.65 86.5 -57.3 43.65L-62.25 44.3Q-36.75 84.35 39.25 75.7Q-49.1 94.95 -67.4 45.5L-74.5 44.6Q-56.9 93.4 17.65 103.85Q-70 101.3 -78.45 43.95Q-83.15 42.95 -87.45 40.9Q-78.8 99.95 -5.6 130.9Q-92.2 105.25 -89.95 40.25L-91.75 39.4Q-98.65 103.8 -27.95 154.65Q-109.7 105 -94.95 36.3L-92.45 27.35Q-93.05 33.9 -92.05 34.75Q-91.1 35.55 -88.95 36.7L-87.95 37Q-83.7 38.25 -79.05 38.8L-77.25 38.95Q-72.55 39.3 -67.5 38.85L-65.45 38.65Q-44.4 36.05 -17.8 19.6Q-9.9 12.8 -15.15 4.4Q-18.15 3.15 -19 0Q-18.15 -3.15 -15.15 -4.4Q-9.9 -12.8 -17.8 -19.6L-17.8 -19.55Q-44.4 -36.05 -65.45 -38.6L-67.5 -38.8Q-72.55 -39.3 -77.25 -38.95L-79.05 -38.75Q-83.7 -38.25 -87.95 -36.95L-88.95 -36.65Q-91.1 -35.55 -92.05 -34.7Q-93.05 -33.9 -92.45 -27.35M-8.6,15.6Q-17.8 23.3 -21.25 24.2Q-28.3 29.9 -39.2 33.3Q-52.45 38.85 -66.6 40.5Q-79.85 42 -92.35 36Q-93.5 35.65 -93.25 31.55L-94.05 36.1Q-96.1 46.05 -96 56.15Q-95.8 70.5 -90.8 84.1Q-86 97.3 -77.9 108.75Q-70.3 119.35 -61.1 128.6Q-65.95 123.1 -70.25 117.25Q-74.6 111.35 -78.45 105.15Q-82.1 99.3 -85 93Q-88.05 86.55 -90.05 79.7Q-92.15 72.5 -93.25 65.1Q-94.4 58.2 -94.1 51.2Q-93.75 44.35 -92.7 37.6L-89.8 38.9L-88.95 39.45Q-88.65 54.15 -87.55 59.55Q-80.65 94.5 -36.65 117.25Q-79.8 89.8 -86.3 53.1Q-87.65 46.65 -88 39.65Q-82.9 41.75 -77.45 42.75Q-75.75 50.75 -73.65 57.15Q-63.75 86.35 -17 98.3Q-24.05 95.95 -30.6 92.75L-43.65 85.9Q-49.9 82.35 -55.45 77.6Q-60.8 73 -64.8 67.15Q-68.9 61.25 -71.85 54.85Q-74.6 49.1 -76.35 42.95L-70.55 43.6L-67 43.6Q-62.95 52.1 -60.5 55.4L-56.8 60.35Q-54.55 63.35 -48.8 66.9Q-43.05 70.4 -36.5 73Q-29.85 75.65 -22.85 77.15Q-16.2 78.55 -9.45 79.15Q-2.2 79.8 5.1 79.35Q-45.9 75.55 -60.6 50.3L-64.7 43.4L-56.05 41.95Q-53.85 45.65 -50.25 50Q-32.2 72.1 21 57.85Q-34.85 68.4 -51.15 44.85L-53.7 41.2L-45.35 38.35L-42.05 40.95Q-17.75 61.45 29.75 36.85Q19.85 40.9 12.75 42.9Q5.95 44.75 -1 46Q-8.35 47.35 -16.2 46.35Q-24.05 45.3 -31.5 43.3Q-38.9 41.25 -42.55 37.05L-34.25 34.05Q-24.55 30.7 -16.75 24.5Q-12.1 20.75 -8.6 15.6"/> 172 `; 173 174 // Espina (spine) 175 const espina = document.createElementNS(xmlns, 'g'); 176 espina.id = 'Espina'; 177 const espinaGradient1 = document.createElementNS(xmlns, 'linearGradient'); 178 espinaGradient1.id = 'LinearGradID_2'; 179 espinaGradient1.setAttribute('gradientUnits', 'userSpaceOnUse'); 180 espinaGradient1.setAttribute('gradientTransform', 'matrix(0.0935974, 0, 0, 0.188782, -20.55, 0)'); 181 espinaGradient1.setAttribute('spreadMethod', 'pad'); 182 espinaGradient1.setAttribute('x1', '-819.2'); 183 espinaGradient1.setAttribute('x2', '819.2'); 184 espinaGradient1.setAttribute('y1', '0'); 185 espinaGradient1.setAttribute('y2', '0'); 186 espinaGradient1.innerHTML = ` 187 <stop offset="0" style="stop-color: ${dragonParams.spineGradientStart}; stop-opacity: 1"/> 188 <stop offset="1" style="stop-color: ${dragonParams.spineGradientEnd}; stop-opacity: 1"/> 189 `; 190 const espinaGradient2 = document.createElementNS(xmlns, 'linearGradient'); 191 espinaGradient2.id = 'LinearGradID_3'; 192 espinaGradient2.setAttribute('gradientUnits', 'userSpaceOnUse'); 193 espinaGradient2.setAttribute('gradientTransform', 'matrix(0.0229492, 0, 0, 0.0152893, 0, -0.05)'); 194 espinaGradient2.setAttribute('spreadMethod', 'pad'); 195 espinaGradient2.setAttribute('x1', '-819.2'); 196 espinaGradient2.setAttribute('x2', '819.2'); 197 espinaGradient2.setAttribute('y1', '0'); 198 espinaGradient2.setAttribute('y2', '0'); 199 espinaGradient2.innerHTML = ` 200 <stop offset="0" style="stop-color: ${dragonParams.spineGradientStart}; stop-opacity: 1"/> 201 <stop offset="1" style="stop-color: ${dragonParams.spineGradientEnd}; stop-opacity: 1"/> 202 `; 203 espina.innerHTML = ` 204 <path style="fill: url(#LinearGradID_2)" d="M-18.8,0Q-17.85 -5.7 -12.3 -9.6Q-11.2 -5.35 -6.5 -8.25L-6.45 -8.2L-6.2 -8.3Q1.25 -16.25 6.65 -12.4Q0.05 -12.55 0 -5.95Q2.7 -2.4 7.75 -4.1Q18 -1.45 18.8 0L-18.8 0"/> 205 <path style="fill: url(#LinearGradID_3)" d="M18.8,0Q18 1.45 7.75 4.1Q2.7 2.4 0 5.95Q0.05 12.55 6.65 12.4Q1.25 16.25 -6.2 8.35Q-6.35 8.25 -6.45 8.25L-6.5 8.25Q-11.2 5.35 -12.3 9.6Q-17.85 5.7 -18.8 0L18.8 0"/> 206 `; 207 208 defs.appendChild(aletasGradient); 209 defs.appendChild(espinaGradient1); 210 defs.appendChild(espinaGradient2); 211 defs.appendChild(cabeza); 212 defs.appendChild(aletas); 213 defs.appendChild(espina); 214 215 screen = document.createElementNS(xmlns, 'g'); 216 screen.id = 'screen'; 217 218 svgContainer.appendChild(defs); 219 svgContainer.appendChild(screen); 220 svgContainer.style.display = 'none'; // Start hidden, will be shown by switcher 221 document.body.appendChild(svgContainer); 222 // SVG container created 223 } else { 224 // Container exists - regenerate defs with new colors 225 const defs = svgContainer.querySelector('defs'); 226 if (defs) { 227 // Update existing gradients 228 const finGradient = defs.querySelector('#LinearGradID_1'); 229 if (finGradient) { 230 const stops = finGradient.querySelectorAll('stop'); 231 if (stops[0]) stops[0].setAttribute('style', `stop-color: ${dragonParams.finGradientStart}; stop-opacity: 1`); 232 if (stops[1]) stops[1].setAttribute('style', `stop-color: ${dragonParams.finGradientEnd}; stop-opacity: 1`); 233 } 234 235 const spineGradient1 = defs.querySelector('#LinearGradID_2'); 236 if (spineGradient1) { 237 const stops = spineGradient1.querySelectorAll('stop'); 238 if (stops[0]) stops[0].setAttribute('style', `stop-color: ${dragonParams.spineGradientStart}; stop-opacity: 1`); 239 if (stops[1]) stops[1].setAttribute('style', `stop-color: ${dragonParams.spineGradientEnd}; stop-opacity: 1`); 240 } 241 242 const spineGradient2 = defs.querySelector('#LinearGradID_3'); 243 if (spineGradient2) { 244 const stops = spineGradient2.querySelectorAll('stop'); 245 if (stops[0]) stops[0].setAttribute('style', `stop-color: ${dragonParams.spineGradientStart}; stop-opacity: 1`); 246 if (stops[1]) stops[1].setAttribute('style', `stop-color: ${dragonParams.spineGradientEnd}; stop-opacity: 1`); 247 } 248 249 // Update head colors 250 const cabeza = defs.querySelector('#Cabeza'); 251 if (cabeza) { 252 const paths = cabeza.querySelectorAll('path'); 253 if (paths[0]) paths[0].setAttribute('style', `fill: ${dragonParams.headColor}; fill-opacity: 1`); 254 if (paths[1]) paths[1].setAttribute('style', `fill: ${dragonParams.headOutlineColor}; fill-opacity: 1`); 255 } 256 } 257 258 // Find or create screen element 259 screen = svgContainer.querySelector('#screen') || svgContainer.querySelector('g[i
259d="screen"]'); 260 if (!screen) { 261 screen = document.createElementNS(xmlns, 'g'); 262 screen.id = 'screen'; 263 svgContainer.appendChild(screen); 264 } 265 // Clear any existing elements 266 while (screen.firstChild) { 267 screen.removeChild(screen.firstChild); 268 } 269 // Using existing SVG container, updated colors 270 } 271 272 // Verify container exists 273 if (!svgContainer) { 274 console.error('ð Dragon: Failed to find SVG container after creation!'); 275 return; 276 } 277 // Initialize dimensions 278 width = window.innerWidth; 279 height = window.innerHeight; 280 281 // Always generate random dragon parameters on init 282 dragonParams = generateDragonParams(); 283 284 // Initialize pointer to center of screen 285 pointer.x = width / 2; 286 pointer.y = height / 2; 287 radm = Math.min(pointer.x, pointer.y) - 20; 288 289 // Initialize elements array in pixel coordinates 290 const N = dragonParams.segmentCount; 291 elems = []; 292 for (let i = 0; i < N; i++) { 293 elems[i] = { use: null, x: width / 2, y: height / 2 }; // Start at center, not top 294 } 295 296 rad = 0; 297 frm = Math.random(); 298 299 // Create dragon segments with procedural fin placement 300 for (let i = 1; i < N; i++) { 301 if (i === 1) prepend("Cabeza", i); 302 else if (dragonParams.finPositions.includes(i)) prepend("Aletas", i); 303 else prepend("Espina", i); 304 } 305 306 // Event listeners 307 window.addEventListener('pointermove', handlePointerMove, false); 308 window.addEventListener('resize', handleResize, false); 309 310 // Set initialized BEFORE starting animation 311 isInitialized = true; 312 313 // Only log on first initialization 314 if (!window.DragonFollower || !window.DragonFollower._hasLoggedInit) { 315 console.log(`ð Dragon: Initialized with ${dragonParams.segmentCount} segments, ${dragonParams.finPositions.length} fins`); 316 if (window.DragonFollower) window.DragonFollower._hasLoggedInit = true; 317 } 318 319 // Start animation AFTER initialization is complete 320 // Use setTimeout to ensure DOM is ready 321 setTimeout(() => { 322 // Double-check svgContainer is accessible 323 if (!svgContainer) { 324 svgContainer = document.getElementById('dragon-follower'); 325 } 326 run(); 327 }, 0); 328 }; 329 330 const handlePointerMove = (e) => { 331 pointer.x = e.clientX; 332 pointer.y = e.clientY; 333 rad = 0; 334 335 // Update cursor history for Python AI 336 cursorHistory.push({x: pointer.x, y: pointer.y, time: Date.now()}); 337 if (cursorHistory.length > CURSOR_HISTORY_SIZE) { 338 cursorHistory.shift(); 339 } 340 341 // Don't log pointer movements - too spammy 342 }; 343 344 const handleResize = () => { 345 resize(); 346 // Update viewBox when window resizes 347 if (svgContainer) { 348 svgContainer.setAttribute('viewBox', `0 0 ${width} ${height}`); 349 // ViewBox resized 350 } 351 }; 352 353 let frameCount = 0; 354 const run = async () => { 355 // Re-check svgContainer reference in case it wasn't set 356 if (!svgContainer) { 357 svgContainer = document.getElementById('dragon-follower'); 358 } 359 360 if (!isInitialized || !screen || !svgContainer) { 361 if (frameCount === 0) { 362 console.error('ð Dragon: Animation blocked - isInitialized:', isInitialized, 'screen:', !!screen, 'svgContainer:', !!svgContainer); 363 } 364 frameCount++; 365 if (frameCount < 10) { 366 animationId = requestAnimationFrame(run); 367 } 368 return; 369 } 370 371 animationId = requestAnimationFrame(run); 372 373 // Animation loop running 374 375 let e = elems[0]; 376 if (!e) { 377 console.error('ð Dragon: No elements in array!'); 378 return; 379 } 380 381 // Default behavior 382 const oldX = e.x; 383 const oldY = e.y; 384 const ax = (Math.cos(3 * frm) * rad * width) / height; 385 const ay = (Math.sin(4 * frm) * rad * height) / width; 386 387 e.x += (ax + pointer.x - e.x) / 10;
388 e.y += (ay + pointer.y - e.y) / 10; 389 390 // Reduced logging - only log every 600 frames (10 seconds at 60fps) 391 if (frameCount === 0 || (frameCount % 600 === 0 && frameCount > 0)) { 392 console.log(`ð Dragon: Frame ${frameCount} - Element 0 at (${e.x.toFixed(1)}, ${e.y.toFixed(1)}), pointer: (${pointer.x.toFixed(1)}, ${pointer.y.toFixed(1)})`); 393 } 394 395 for (let i = 1; i < elems.length; i++) { 396 let e = elems[i]; 397 let ep = elems[i - 1]; 398 399 if (!e || !ep) continue; 400 401 const a = Math.atan2(e.y - ep.y, e.x - ep.x); 402 e.x += (ep.x - e.x + (Math.cos(a) * (100 - i)) / dragonParams.segmentSpacing) / 4; 403 e.y += (ep.y - e.y + (Math.sin(a) * (100 - i)) / dragonParams.segmentSpacing) / 4; 404 405 // Use procedural scale 406 const s = ((162 + 4 * (1 - i)) / 50) * dragonParams.scale; 407 408 if (e.use) { 409 e.use.setAttributeNS( 410 null, 411 "transform", 412 `translate(${(ep.x + e.x) / 2},${(ep.y + e.y) / 2}) rotate(${ 413 (180 / Math.PI) * a 414 }) translate(${0},${0}) scale(${s},${s})` 415 ); 416 } else if (frameCount < 5 && i < 5) { 417 console.warn(`ð Dragon: Element ${i} has no use element!`); 418 } 419 } 420 421 if (rad < radm) rad++; 422 frm += dragonParams.animationSpeed; 423 424 if (rad > 60) { 425 pointer.x += (width / 2 - pointer.x) * 0.05; 426 pointer.y += (height / 2 - pointer.y) * 0.05; 427 } 428 429 frameCount++; 430 }; 431 432 const cleanup = () => { 433 if (!isInitialized) return; 434 435 // Cancel animation 436 if (animationId) { 437 cancelAnimationFrame(animationId); 438 animationId = null; 439 } 440 441 // Remove event listeners 442 window.removeEventListener('pointermove', handlePointerMove, false); 443 window.removeEventListener('resize', handleResize, false); 444 445 // Clear elements 446 elems = []; 447 pointer = { x: 0, y: 0 }; 448 rad = 0; 449 frm = Math.random(); 450 451 // Hide and optionally remove SVG container (will be recreated on next init) 452 const svgContainer = document.getElementById('dragon-follower'); 453 if (svgContainer) { 454 svgContainer.style.display = 'none'; 455 // Remove from DOM to force regeneration of defs 456 if (svgContainer.parentNode) { 457 svgContainer.parentNode.removeChild(svgContainer); 458 } 459 } 460 461 isInitialized = false; 462 console.log('ð Dragon follower cleaned up'); 463 }; 464 465 const regenerate = () => { 466 // Generate new random parameters first 467 dragonParams = generateDragonParams(); 468 // Clean up and reinitialize 469 cleanup(); 470 // Small delay to ensure cleanup is complete 471 setTimeout(() => { 472 init(); 473 // Make sure the dragon is visible after regeneration 474 // The container will be created with display: 'none', so we need to show it 475 setTimeout(() => { 476 const dragonContainer = document.getElementById('dragon-follower'); 477 if (dragonContainer && isInitialized) { 478 dragonContainer.style.display = ''; 479 dragonContainer.style.opacity = '1'; 480 // Dragon made visible after regeneration 481 } 482 }, 50); 483 }, 100); 484 }; 485 486 // Global API 487 window.DragonFollower = { 488 init: init, 489 cleanup: cleanup, 490 regenerate: regenerate, 491 isActive: () => isInitialized, 492 getParams: () => ({ ...dragonParams }), // Return copy of current params 493 setMaxSegments: (max) => { 494 if (typeof max === 'number' && max >= MIN_SEGMENTS && max <= 100) { 495 const oldMax = maxSegments; 496 maxSegments = max; 497 // Max segments updated 498 return true; 499 } else { 500 console.warn(`ð Dragon: Invalid max segments value. Must be between ${MIN_SEGMENTS} and 100.`); 501 return false; 502 } 503 }, 504 getMaxSegments: () => maxSegments, 505 getMinSegments: () => MIN_SEGMENTS 506 }; 507 508})(); 509
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.