PageSourceSearch

https://pupwife.com/scripts/followers/dragonFollower.js

js pupwife.com collected 2026-09-30 15:05:23 UTC 25,954 bytes, 509 lines download raw bytes

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.