PageSourceSearch

https://pupwife.com/scripts/dependencies/cursors/twojsCursor.js

js pupwife.com collected 2026-09-30 15:05:35 UTC 13,061 bytes, 350 lines download raw bytes

1/**
2 * Two.js Custom Cursor System
3 * Main manager for all Two.js-based cursor implementations
4 */
5
6(function() {
7    'use strict';
8
9    function emitDevLog() {
10        if (typeof window !== 'undefined' && typeof window.devLog === 'function') {
11            window.devLog.apply(window, arguments);
12        }
13    }
14    function emitDevWarn() {
15        if (typeof window !== 'undefined' && typeof window.devWarn === 'function') {
16            window.devWarn.apply(window, arguments);
17        }
18    }
19
20    let currentCursor = null;
21    let currentCursorType = 'none';
22    let twoInstance = null;
23    let cursorContainer = null;
24    let isInitialized = false;
25    let lastFrameTime = 0;
26    let deltaTime = 0;
27
28    // Cursor type definitions
29    const CURSOR_TYPES = {
30        none: null,
31        'twojs-geometric': 'animatedGeometricCursor',
32        'twojs-particles': 'interactiveParticleCursor',
33        'twojs-morphing': 'morphingShapeCursor',
34        'twojs-physics': 'physicsBasedCursor',
35        'twojs-reactive': 'reactiveCursor',
36        'twojs-recursive': 'recursiveCursor'
37    };
38
39    /**
40     * Initialize the Two.js cursor system
41     */
42    function init() {
43        if (isInitialized) return;
44        
45        // Wait for Two.js to be available
46        if (typeof Two === 'undefined') {
47            emitDevWarn('Two.js not loaded yet, waiting...');
48            setTimeout(init, 100);
49            return;
50        }
51
52        // Create cursor container
53        cursorContainer = document.createElement('div');
54        cursorContainer.id = 'twojs-cursor-container';
55        cursorContainer.style.cssText = `
56            position: fixed;
57            top: 0;
58            left: 0;
59            width: 100%;
60            height: 100%;
61            pointer-events: none !important;
62            z-index: 101600;
63            cursor: none;
64        `;
65        // Ensure all children also have pointer-events: none
66        cursorContainer.setAttribute('data-pointer-events', 'none');
67        document.body.appendChild(cursorContainer);
68        
69        // Add CSS to ensure all children inherit pointer-events: none
70        const containerStyle = document.createElement('style');
71        containerStyle.id = 'twojs-cursor-container-style';
72        containerStyle.textContent = `
73            #twojs-cursor-container,
74            #twojs-cursor-container *,
75            #twojs-cursor-container svg,
76            #twojs-cursor-container canvas,
77            #twojs-cursor-container > svg,
78            #twojs-cursor-container > div {
79                pointer-events: none !important;
80                touch-action: none !important;
81            }
82        `;
83        document.head.appendChild(containerStyle);
84
85        // Hide default cursor when Two.js cursor is active
86        const style = document.createElement('style');
87        style.id = 'twojs-cursor-style';
88        style.textContent = `
89            body.twojs-cursor-active * {
90                cursor: none !important;
91            }
92        `;
93        document.head.appendChild(style);
94
95        isInitialized = true;
96        emitDevLog('✨ Two.js cursor system initialized');
97    }
98
99    const TWO_JS_SRC = 'https://cdn.jsdelivr.net/npm/[email protected]/build/two.min.js';
100    let twoJsLoadPromise = null;
101
102    function ensureTwoJs() {
103        if (typeof Two !== 'undefined') {
104            return Promise.resolve();
105        }
106        if (twoJsLoadPromise) return twoJsLoadPromise;
107        twoJsLoadPromise = new Promise(function (resolve) {
108            var existing = document.querySelector('script[src="' + TWO_JS_SRC + '"]');
109            if (existing) {
110                existing.addEventListener('load', function () { resolve(); }, { once: true });
111                existing.addEventListener('error', function () { resolve(); }, { once: true });
112                setTimeout(resolve, 3000);
113                return;
114            }
115            var script = document.createElement('script');
116            script.src = TWO_JS_SRC;
117            script.async = false;
118            script.onload = function () { resolve(); };
119            script.onerror = function () {
120                emitDevWarn('Two.js failed to load — physics/geometric cursors unavailable.');
121                resolve();
122            };
123            document.head.appendChild(script);
124        });
125        return twoJsLoadPromise;
126    }
127
128    /**
129     * Load a cursor module dynamically
130     */
131    function loadCursorModule(cursorName) {
132        return new Promise((resolve, reject) => {
133            const script = document.createElement('script');
134            script.src = `/scripts/dependencies/cursors/${cursorName}.js`;
135            script.onload = () => resolve();
136            script.onerror = () => reject(new Error(`Failed to load ${cursorName}`));
137            document.head.appendChild(script);
138        });
139    }
140
141    /**
142     * Set the active cursor type
143     */
144    async function setCursor(cursorType) {
145        // Clean up current cursor
146        if (currentCursor && typeof currentCursor.destroy === 'function') {
147            currentCursor.destroy();
148            currentCursor = null;
149        }
150
151        if (twoInstance) {
152            twoInstance.pause();
153            // Unbind update handler
154            twoInstance.unbind('update');
155            // Clean up resize handler
156            if (twoInstance._resizeHandler) {
157                window.removeEventListener('resize', twoInstance._resizeHandler);
158            }
159            // Clean up observer
160            if (twoInstance._pointerEventsObserver) {
161                twoInstance._pointerEventsObserver.disconnect();
162            }
163            // Ensure renderer is cleaned up properly
164            if (twoInstance.renderer && twoInstance.renderer.domElement) {
165                twoInstance.renderer.domElement.style.pointerEvents = 'none';
166            }
167            if (cursorContainer) {
168                cursorContainer.innerHTML = '';
169            }
170            twoInstance = null;
171        }
172
173        // Remove body class
174        document.body.classList.remove('twojs-cursor-active');
175
176        currentCursorType = cursorType;
177
178        // If none, we're done
179        if (cursorType === 'none' || !cursorType) {
180            return;
181        }
182
183        // Initialize if needed
184        if (!isInitialized) {
185            init();
186            // Wait a bit for initialization
187            await new Promise(resolve => setTimeout(resolve, 50));
188        }
189
190        // Get cursor module name
191        const moduleName = CURSOR_TYPES[cursorType];
192        if (!moduleName) {
193            emitDevWarn(`Unknown cursor type: ${cursorType}`);
194            return;
195        }
196
197        try {
198            await ensureTwoJs();
199            if (typeof Two === 'undefined') {
200                emitDevWarn('Two.js is not available — cannot use cursor: ' + cursorType);
201                return;
202            }
203
204            // Load the cursor module if it exists
205            if (typeof window[moduleName] === 'undefined') {
206                await loadCursorModule(moduleName);
207            }
208
209            // Create Two.js instance - use explicit dimensions instead of fullscreen
210            const params = {
211                width: window.innerWidth,
212                height: window.innerHeight,
213                autostart: true,
214                type: Two.Types.svg
215            };
216            twoInstance = new Two(params).appendTo(cursorContainer);
217            
218            // Immediately set pointer-events on the renderer's DOM element
219            if (twoInstance.renderer && twoInstance.renderer.domElement) {
220                const domElement = twoInstance.renderer.domElement;
221                domElement.style.pointerEvents = 'none';
222                domElement.style.setProperty('pointer-events', 'n
222one', 'important');
223                
224                // Also set on the element itself via setAttribute for maximum compatibility
225                domElement.setAttribute('style', domElement.getAttribute('style') + '; pointer-events: none !important;');
226                
227                // Use MutationObserver to ensure it stays set
228                const observer = new MutationObserver(() => {
229                    if (domElement.style.pointerEvents !== 'none') {
230                        domElement.style.pointerEvents = 'none';
231                        domElement.style.setProperty('pointer-events', 'none', 'important');
232                    }
233                });
234                observer.observe(domElement, { attributes: true, attributeFilter: ['style'] });
235                
236                // Store observer for cleanup
237                twoInstance._pointerEventsObserver = observer;
238            }
239            
240            // Handle window resize
241            const resizeHandler = () => {
242                if (twoInstance) {
243                    twoInstance.width = window.innerWidth;
244                    twoInstance.height = window.innerHeight;
245                }
246            };
247            window.addEventListener('resize', resizeHandler);
248            twoInstance._resizeHandler = resizeHandler;
249
250            // Create cursor instance
251            // Try different naming conventions
252            let CursorClass = window[moduleName];
253            if (!CursorClass) {
254                // Try with first letter capitalized
255                const capitalized = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
256                CursorClass = window[capitalized];
257            }
258            
259            if (CursorClass && typeof CursorClass === 'function') {
260                currentCursor = new CursorClass(twoInstance);
261                // Some cursors (e.g. recursive) keep the real OS pointer as the lead
262                if (!currentCursor.keepNativeCursor) {
263                    document.body.classList.add('twojs-cursor-active');
264                }
265                
266                // Initialize time tracking
267                lastFrameTime = performance.now();
268                
269                // Hook into Two.js update cycle to calculate deltaTime
270                twoInstance.bind('update', function() {
271                    const currentTime = performance.now();
272                    deltaTime = (currentTime - lastFrameTime) / 1000; // Convert to seconds
273                    lastFrameTime = currentTime;
274                    
275                    // Clamp deltaTime to prevent large jumps (e.g., when tab becomes visible)
276                    deltaTime = Math.min(deltaTime, 0.1); // Max 100ms
277                    
278                    // Call cursor update with deltaTime
279                    if (currentCursor && typeof currentCursor.update === 'function') {
280                        currentCursor.update(deltaTime);
281                    }
282                });
283                
284                emitDevLog(`🎯 Two.js cursor set to: ${cursorType}`);
285            } else {
286                console.error(`Cursor class ${moduleName} not found. Available:`, Object.keys(window).filter(k => k.includes('Cursor')));
287            }
288        } catch (error) {
289            console.error(`Failed to load cursor ${cursorType}:`, error);
290        }
291    }
292
293    /**
294     * Get current cursor type
295     */
296    function getCursor() {
297        return currentCursorType;
298    }
299
300    /**
301     * Cleanup
302     */
303    function cleanup() {
304        if (currentCursor && typeof currentCursor.destroy === 'function') {
305            currentCursor.destroy();
306        }
307        if (twoInstance) {
308            twoInstance.pause();
309            // Clean up resize handler
310            if (twoInstance._resizeHandler) {
311                window.removeEventListener('resize', twoInstance._resizeHandler);
312            }
313            // Clean up observer
314            if (twoInstance._pointerEventsObserver) {
315                twoInstance._pointerEventsObserver.disconnect();
316            }
317            if (twoInstance.renderer && twoInstance.renderer.domElement) {
318                twoInstance.renderer.domElement.style.pointerEvents = 'none';
319            }
320        }
321        if (cursorContainer && cursorContainer.parentNode) {
322            cursorContainer.parentNode.removeChild(cursorContainer);
323        }
324        // Remove container style
325        const containerStyle = document.getElementById('twojs-cursor-container-style');
326        if (containerStyle) {
327            containerStyle.remove();
328        }
329        document.body.classList.remove('twojs-cursor-active');
330        isInitialized = false;
331    }
332
333    // Export API
334    window.TwoJSCursor = {
335        init,
336        setCursor,
337        getCursor,
338        cleanup,
339        CURSOR_TYPES
340    };
341
342    // Auto-initialize when DOM is ready
343    if (document.readyState === 'loading') {
344        document.addEventListener('DOMContentLoaded', init);
345    } else {
346        init();
347    }
348
349})();
350

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.