1/** 2 * Orca Cursor Follower 3 * Inspired by whalefollowmouse.com 4 * Refactored for integration with follower switcher 5 */ 6 7(function() { 8 'use strict'; 9 10 // Physics constants 11 const HORIZONTAL_DIVISOR = 40; 12 const VERTICAL_DIVISOR = 40; 13 const SPEED = 60; // Slower speed to keep frames tightly together 14 const MOBILE_SPEED = 40; // Slower speed on mobile 15 const MIN_DISTANCE = 10; // Tight frame spacing (1/5th of original 50) 16 const THRESHOLD_DISTANCE = 1; // Tight frame spacing (1/5th of original 15) 17 const THRESHOLD_ANGLE = 1; 18 const FPS = 60; 19 const FRAME_TIME = 1000 / FPS; 20 const MOBILE_BREAKPOINT = 425; 21 const TABLET_BREAKPOINT = 640; 22 const SCALE_DESKTOP = 0.2; // 1/5th size for desktop 23 const SCALE_TABLET = 0.12; // 1/5th size for tablet 24 const SCALE_MOBILE = 0.17; // 1/5th size for mobile 25 const IMAGE_OFFSET_X = 2; 26 const IMAGE_OFFSET_Y = 1.7; 27 const FRAME_COUNT = 44; 28 29 // Image paths for all 44 frames 30 const FRAME_PATHS = [...Array(FRAME_COUNT).keys()].map(i => `/images/orca/${i + 1}.webp`); 31 32 // Module state 33 let canvas = null; 34 let ctx = null; 35 let images = []; 36 let mouseX = 0; 37 let mouseY = 0; 38 let speed = SPEED; 39 let lastFrameTime = Date.now(); 40 let animationId = null; 41 let isInitialized = false; 42 43 // Orca state 44 const orcaState = { 45 layerPositions: [], 46 orca: { x: 0, y: 0 }, 47 mouseOffset: { x: 0, y: 0 } 48 }; 49 50 // Event handlers 51 let mouseMoveHandler = null; 52 let touchMoveHandler = null; 53 let resizeHandler = null; 54 55 /** 56 * Load all orca frame images 57 */ 58 function loadImages(scale) { 59 const imagePromises = FRAME_PATHS.map((path, index) => { 60 return new Promise((resolve, reject) => { 61 const img = new Image(); 62 img.onload = () => { 63 img.width = img.naturalWidth * scale; 64 img.height = img.naturalHeight * scale; 65 resolve({ img, id: index }); 66 }; 67 img.onerror = () => reject({ img, id: index }); 68 img.src = path; 69 }); 70 }); 71 72 return Promise.all(imagePromises); 73 } 74 75 /** 76 * Sort images by ID in descending order 77 */ 78 function sortImages(loadedImages) { 79 loadedImages.sort((a, b) => a.id > b.id ? -1 : 1); 80 } 81 82 /** 83 * Calculate movement vector towards target 84 */ 85 function calculateMovement(target, current, currentSpeed) { 86 const dx = target.x - current.x; 87 const dy = target.y - current.y; 88 const angle = Math.atan2(dy, dx); 89 const distance = Math.sqrt(dx * dx + dy * dy); 90 91 if (distance > THRESHOLD_DISTANCE) { 92 const movement = polarToCartesian(currentSpeed, angle); 93 return { 94 x: movement.x / HORIZONTAL_DIVISOR, 95 y: movement.y / VERTICAL_DIVISOR 96 }; 97 } else if (angle > THRESHOLD_ANGLE) { 98 const movement = polarToCartesian(MIN_DISTANCE, angle); 99 return { 100 x: movement.x / HORIZONTAL_DIVISOR, 101 y: movement.y / VERTICAL_DIVISOR 102 }; 103 } 104 105 return { x: dx, y: dy }; 106 } 107 108 /** 109 * Convert polar coordinates to cartesian 110 */ 111 function polarToCartesian(radius, angle) { 112 return { 113 x: radius * Math.cos(angle), 114 y: radius * Math.sin(angle) 115 }; 116 } 117 118 /** 119 * Create array filled with a value 120 */ 121 function fillArray(value) { 122 return [...Array(FRAME_COUNT).keys()].map(() => value); 123 } 124 125 /** 126 * Get appropriate scale based on window size 127 */ 128 function getScale() { 129 if (window.matchMedia(`screen and (max-width: ${MOBILE_BREAKPOINT}px)`).matches) { 130 return SCALE_TABLET; 131 } 132 if (window.matchMedia(`screen and (max-width: ${TABLET_BREAKPOINT}px)`).matches) { 133 return SCALE_MOBILE; 134 } 135 return SCALE_DESKTOP; 136 } 137 138 /** 139 * Initialize orca positions 140 */ 141 function initializePositions() { 142 const centerX = window.innerWidth / 2; 143 const centerY = window.innerHeight / 2; 144 145 orcaState.layerPositions = fillArray({ x: centerX, y: centerY });
146 orcaState.orca.x = centerX; 147 orcaState.orca.y = centerY; 148 mouseX = centerX; 149 mouseY = centerY; 150 } 151 152 /** 153 * Handle window resize 154 */ 155 function handleResize() { 156 if (!canvas) return; 157 canvas.width = window.innerWidth; 158 canvas.height = window.innerHeight; 159 } 160 161 /** 162 * Handle mouse movement 163 */ 164 function handleMouseMove(e) { 165 mouseX = e.clientX; 166 mouseY = e.clientY; 167 } 168 169 /** 170 * Handle touch movement 171 */ 172 function handleTouchMove(e) { 173 if (e.targetTouches.length > 0) { 174 mouseX = e.targetTouches[0].clientX; 175 mouseY = e.targetTouches[0].clientY; 176 } 177 } 178 179 /** 180 * Update orca positions 181 */ 182 function updatePositions() { 183 const target = { 184 x: mouseX - orcaState.mouseOffset.x, 185 y: mouseY - orcaState.mouseOffset.y 186 }; 187 188 const movement = calculateMovement(target, orcaState.orca, speed); 189 190 orcaState.orca.x += movement.x; 191 orcaState.orca.y += movement.y; 192 193 orcaState.layerPositions.push({ 194 x: orcaState.orca.x, 195 y: orcaState.orca.y 196 }); 197 orcaState.layerPositions.shift(); 198 } 199 200 /** 201 * Render orca frames 202 */ 203 function render() { 204 if (!ctx || !canvas) return; 205 206 // Clear canvas 207 ctx.clearRect(0, 0, canvas.width, canvas.height); 208 209 // Draw all layers 210 orcaState.layerPositions.forEach((pos, index) => { 211 const img = images[index].img; 212 const width = img.width; 213 const height = img.height; 214 215 ctx.drawImage( 216 img, 217 pos.x - width / IMAGE_OFFSET_X, 218 pos.y - height / IMAGE_OFFSET_Y, 219 width, 220 height 221 ); 222 }); 223 } 224 225 /** 226 * Animation loop 227 */ 228 function animate() { 229 if (!isInitialized) return; 230 231 const now = Date.now(); 232 const elapsed = now - lastFrameTime; 233 234 // Update positions at target FPS 235 if (elapsed > FRAME_TIME) { 236 lastFrameTime = now - (elapsed % FRAME_TIME); 237 updatePositions(); 238 } 239 240 render(); 241 animationId = requestAnimationFrame(animate); 242 } 243 244 /** 245 * Set mobile speed if needed 246 */ 247 function setMobileSpeed() { 248 if (window.matchMedia(`screen and (max-width: ${MOBILE_BREAKPOINT}px)`).matches) { 249 speed = MOBILE_SPEED; 250 } 251 } 252 253 /** 254 * Initialize the orca follower 255 */ 256 function init() { 257 if (isInitialized) { 258 console.warn('ð Orca follower already initialized'); 259 return; 260 } 261 262 console.log('ð Initializing orca follower...'); 263 264 // Create canvas 265 canvas = document.createElement('canvas'); 266 canvas.id = 'orca-canvas'; 267 canvas.style.cssText = ` 268 position: fixed; 269 top: 0; 270 left: 0; 271 width: 100%; 272 height: 100%; 273 pointer-events: none; 274 z-index: 9998; 275 `; 276 document.body.appendChild(canvas); 277 278 ctx = canvas.getContext('2d'); 279 if (!ctx) { 280 console.error('ð Failed to get canvas context'); 281 cleanup(); 282 return; 283 } 284 285 canvas.width = window.innerWidth; 286 canvas.height = window.innerHeight; 287 288 // Set speed based on device 289 setMobileSpeed(); 290 291 // Initialize positions 292 initializePositions(); 293 294 // Set up event listeners 295 mouseMoveHandler = handleMouseMove; 296 touchMoveHandler = handleTouchMove; 297 resizeHandler = handleResize; 298 299 window.addEventListener('mousemove', mouseMoveHandler, false); 300 window.addEventListener('touchmove', touchMoveHandler, false); 301 window.addEventListener('resize', resizeHandler, false); 302 303 // Load images and start animation 304 const scale = getScale(); 305 loadImages(scale) 306 .then(loadedImages => { 307 images = loadedImages; 308 sortImages(images); 309 isInitialized = true; 310 animate(); 311 console.log('ð Orca follower initialized successfully!'); 312 }) 313 .catch(err => { 314 console.error('ð Failed to load orca images:', err); 315 cleanup(); 316 }); 317 } 318 319 /** 320 * Clean up the orca follower 321 */ 322 function cleanup() { 323 console.log('ð Cleaning up orca follower...'); 324 325 isInitialized = false; 326 327 // Cancel animation 328 if (animationId) { 329 cancelAnimationFrame(animationId); 330 animationId = null; 331 } 332 333 // Remove event listeners 334 if (mouseMoveHandler) { 335 window.removeEventListener('mousemove', mouseMoveHandler, false); 336 mouseMoveHandler = null; 337 } 338 if (touchMoveHandler) { 339 window.removeEventListener('touchmove', touchMoveHandler, false); 340 touchMoveHandler = null; 341 } 342 if (resizeHandler) { 343 window.removeEventListener('resize', resizeHandler, false); 344 resizeHandler = null; 345 } 346 347 // Remove canvas 348 if (canvas && canvas.parentNode) { 349 canvas.parentNode.removeChild(canvas); 350 } 351 canvas = null; 352 ctx = null; 353 354 // Clear state 355 images = []; 356 orcaState.layerPositions = []; 357 orcaState.orca = { x: 0, y: 0 }; 358 orcaState.mouseOffset = { x: 0, y: 0 }; 359 360 console.log('ð Orca follower cleaned up'); 361 } 362 363 // Expose public API 364 window.OrcaFollower = { 365 init, 366 cleanup 367 }; 368 369})(); 370
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.