1// ==================================== 2// CURSOR TRAILS SYSTEM 3// Multiple cursor trail effects 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 const TRAIL_TYPES = { 21 none: 'none', 22 sparkles: 'sparkles', 23 fairy: 'fairy', 24 rainbow: 'rainbow', 25 bubbles: 'bubbles', 26 hearts: 'hearts', 27 stars: 'stars', 28 fire: 'fire', 29 snow: 'snow', 30 leaves: 'leaves', 31 // NEW TRAILS 32 lightning: 'lightning', 33 cherryBlossoms: 'cherryBlossoms', 34 fireflies: 'fireflies', 35 butterflies: 'butterflies', 36 comets: 'comets', 37 magicRunes: 'magicRunes', 38 crystals: 'crystals', 39 energyOrbs: 'energyOrbs', 40 matrix: 'matrix', 41 neonLines: 'neonLines', 42 glitch: 'glitch', 43 musicNotes: 'musicNotes', 44 confetti: 'confetti', 45 paintSplatter: 'paintSplatter', 46 smoke: 'smoke', 47 ripples: 'ripples', 48 dnaHelix: 'dnaHelix', 49 circuit: 'circuit', 50 binary: 'binary', 51 pawPrints: 'pawPrints', 52 // Heart colors 53 orangeHearts: 'orangeHearts', 54 yellowHearts: 'yellowHearts', 55 blueHearts: 'blueHearts', 56 redHearts: 'redHearts', 57 purpleHearts: 'purpleHearts', 58 greenHearts: 'greenHearts', 59 whiteHearts: 'whiteHearts', 60 blackHearts: 'blackHearts',
61 rainbowHearts: 'rainbowHearts', 62 dandelion: 'dandelion', 63 rosePetals: 'rosePetals', 64 pixelated: 'pixelated', 65 galaxy: 'galaxy', 66 auroraWaves: 'auroraWaves', 67 geometric: 'geometric', 68 spiral: 'spiral', 69 inkBlots: 'inkBlots', 70 paintStrokes: 'paintStrokes', 71 laserBeams: 'laserBeams', 72 holograms: 'holograms', 73 clouds: 'clouds', 74 tornado: 'tornado', 75 emojis: 'emojis', 76 candy: 'candy', 77 electricSparks: 'electricSparks', 78 plasma: 'plasma', 79 vortex: 'vortex', 80 cosmicDust: 'cosmicDust', 81 // NEW PHYSICS & OCEAN TRAILS 82 bouncyBalls: 'bouncyBalls', 83 physicsBoxes: 'physicsBoxes', 84 jellyfish: 'jellyfish', 85 fish: 'fish', 86 acorns: 'acorns', 87 pinecones: 'pinecones', 88 raindrops: 'raindrops', 89 mist: 'mist', 90 fog: 'fog', 91 chalk: 'chalk', 92 pastels: 'pastels', 93 dataPackets: 'dataPackets', 94 soundWaves: 'soundWaves', 95 trafficLights: 'trafficLights', 96 bees: 'bees', 97 lensFlares: 'lensFlares', 98 prisms: 'prisms', 99 // Physics chaos 100 chainLinks: 'chainLinks', 101 dominoes: 'dominoes', 102 // Ocean family 103 seahorses: 'seahorses', 104 starfish: 'starfish', 105 coral: 'coral', 106 // Cozy nature 107 mushrooms: 'mushrooms', 108 feathers: 'feathers', 109 fruit: 'fruit', 110 // Fantasy 111 portals: 'portals', 112 manaOrbs: 'manaOrbs', 113 // Music & food 114 vinylRecords: 'vinylRecords', 115 coffeeBeans: 'coffeeBeans', 116 sprinkles: 'sprinkles', 117 // Casino & chat 118 dice: 'dice', 119 chatBubbles: 'chatBubbles', 120 spaghetti: 'spaghetti', 121 // Extended handlers (cursorTrailHandlers.js) 122 tardigrade: 'tardigrade', 123 seashells: 'seashells', 124 seaweed: 'seaweed', 125 pearlBubbles: 'pearlBubbles', 126 bioluminescence: 'bioluminescence', 127 snowballFight: 'snowballFight', 128 valentineCandy: 'valentineCandy', 129 ropeYarn: 'ropeYarn', 130 // Matter trails (cursorTrailsMatter.js) 131 marbleRun: 'marbleRun', 132 newtonsCradle: 'newtonsCradle', 133 stackingBlocks: 'stackingBlocks', 134 // Batch 3 (cursorTrailHandlers.js) 135 tetrominoes: 'tetrominoes', 136 pokerChips: 'pokerChips', 137 xpOrbs: 'xpOrbs', 138 pixelHearts: 'pixelHearts', 139 paperClips: 'paperClips', 140 stickyNotes: 'stickyNotes', 141 popcorn: 'popcorn', 142 gummyWoims: 'gummyWoims', 143 shootingStars: 'shootingStars', 144 snails: 'snails', 145 moths: 'moths', 146 bats: 'bats', 147 frogs: 'frogs', 148 woims: 'woims', 149 equalizerBars: 'equalizerBars', 150 glitterSparkles: 'glitterSparkles', 151 moonDust: 'moonDust' 152 }; 153 154 const EXTENSION_SCRIPTS = [ 155 '/scripts/effects/cursorTrailHandlers.js', 156 '/scripts/effects/cursorTrailsMatter.js' 157 ]; 158 let extensionScriptsRequested = false; 159 160 function syncExtensionsForCurrentTrail() { 161 var type = currentTrailType; 162 if (type === 'none') return; 163 if (window.PupwifeCursorTrailHandlers) { 164 window.PupwifeCursorTrailHandlers.onTrailChange(type); 165 } 166 if (window.PupwifeCursorTrailsMatter && canvas) { 167 if (isMatterTrail(type)) { 168 window.PupwifeCursorTrailsMatter.onTrailChange(type, canvas.width, canvas.height); 169 } else { 170 window.PupwifeCursorTrailsMatter.destroy(); 171 } 172 } 173 } 174 175 function requestTrailExtensions() { 176 if (extensionScriptsRequested) return; 177 extensionScriptsRequested = true; 178 EXTENSION_SCRIPTS.forEach(function (src) { 179 if (document.querySelector('script[src="' + src + '"]')) return; 180 var script = document.createElement('script'); 181 script.src = src; 182 script.async = true; 183 script.onload = function () { 184 syncExtensionsForCurrentTrail(); 185 }; 186 document.head.appendChild(script); 187 }); 188 } 189 190 function isHandlerTrail(type) { 191 return window.PupwifeCursorTrailHandlers && 192 typeof window.PupwifeCursorTrailHandlers.isHandlerType === 'function' && 193 window.PupwifeCursorTrailHandlers.isHandlerType(type); 194 } 195 196 function isMatterTrail(type) { 197 return window.PupwifeCursorTrailsMatter && 198 typeof window.PupwifeCursorTrailsMatter.isMatterType === 'function' && 199 window.PupwifeCursorTrailsMatter.isMatterType(type); 200 } 201 202 function getHandlerCtx() { 203 return { 204 particles: particles, 205 mouseVelX: mouseVelX, 206 mouseVelY: mouseVelY, 207 lastMouseX: lastMouseX, 208 lastMouseY: lastMouseY 209 }; 210 } 211 212 const CHAT_BUBBLE_TEXTS = [ 213 'lol', ':3c', 'xD', 'omg', 'uwu', 'brb', 'hai', 'yeet', 'mood', 'same', 214 'facts', 'rip', 'lmao', '???', '!!!', 'hehe', 'nice', 'wow', 'ok', 'ty' 215 ]; 216 217 const FRUIT_COLORS = [ 218 { kind: 'apple', color: '#e53935', stem: '#5d4037' }, 219 { kind: 'orange', color: '#ff9800', stem: '#33691e' }, 220 { kind: 'berry', color: '#9c27b0', stem: '#4a148c' }, 221 { kind: 'grape', color: '#7b1fa2', stem: '#4a148c' }, 222 { kind: 'lemon', color: '#ffeb3b', stem: '#827717' } 223 ]; 224 225 const TRAIL_AMOUNT_LABELS = ['Minimum', 'Low', 'Normal', 'High', 'Extreme']; 226 const TRAIL_AMOUNT_PRESETS = [ 227 { spawnChance: 0.15, maxParticles: 40, burst: 1, speedMin: 2.2 }, 228 { spawnChance: 0.38, maxParticles: 85, burst: 1, speedMin: 1.6 }, 229 { spawnChance: 0.68, maxParticles: 155, burst: 1, speedMin: 1 }, 230 { spawnChance: 0.9, maxParticles: 245, burst: 2, speedMin: 0.75 }, 231 { spawnChance: 1, maxParticles: 400, burst: 3, speedMin: 0.5 } 232 ]; 233 234 let trailColorOverrides = { primary: '#c8a8ff', secondary: '#ffb3ba', tertiary: '#89cff0' }; 235 236 function mergeTrailColors(raw) { 237 if (window.PupwifeTrailColorSupport && typeof window.PupwifeTrailColorSupport.mergeTrailColors === 'function') { 238 return window.PupwifeTrailColorSupport.mergeTrailColors(raw); 239 } 240 return Object.assign({}, trailColorOverrides, raw || {}); 241 } 242 243 function supportsCurrentTrailColors() { 244 return window.PupwifeTrailColorSupport && 245 typeof window.PupwifeTrailColorSupport.supportsTrailColors === 'function' && 246 window.PupwifeTrailColorSupport.supportsTrailColors(currentTrailType); 247 } 248 249 function resolveTrailColor(slotIndex, fallback) { 250 if (!supportsCurrentTrailColors()) return fallback; 251 return window.PupwifeTrailColorSupport.pickColor(trailColorOverrides, slotIndex) || fallback; 252 } 253 254 function tintParticleIfSupported(p, slotIndex) { 255 if (!p || p.color === undefined || !supportsCurrentTrailColors()) return; 256 p.color = resolveTrailColor(slotIndex, p.color); 257 } 258 259 function readTrailColorsFromSettings() { 260 try { 261 const saved = localStorage.getItem('puppywifey_settings'); 262 if (saved) { 263 const parsed = JSON.parse(saved); 264 if (parsed.trailColors) { 265 trailColorOverrides = mergeTrailColors(parsed.trailColors); 266 } 267 } 268 } catch (e) { /* ignore */ } 269 } 270
271 let currentTrailType = 'none'; 272 let currentTrailAmount = 2; 273 let particles = []; 274 let spaghettiPath = []; 275 const SPAGHETTI_MAX_POINTS = 90; 276 const SPAGHETTI_COLORS = ['#f5d76e', '#f4d03f', '#e8c547', '#d4a017', '#c9982a']; 277 let animationFrameId = null; 278 let canvas = null; 279 let ctx = null; 280 let lastMouseX = 0; 281 let lastMouseY = 0; 282 let mouseVelX = 0; 283 let mouseVelY = 0; 284 285 // Cached images for trails 286 let pawPrintImage = null; 287 let pawPrintImageLoaded = false; 288 289 // ==================================== 290 // INITIALIZATION 291 // ==================================== 292 293 function init() { 294 // Create canvas for trails 295 canvas = document.createElement('canvas'); 296 canvas.id = 'cursor-trail-canvas'; 297 canvas.style.cssText = ` 298 position: fixed; 299 top: 0; 300 left: 0; 301 width: 100vw; 302 height: 100vh; 303 pointer-events: none; 304 z-index: 9999; 305 `; 306 document.body.appendChild(canvas); 307 308 ctx = canvas.getContext('2d'); 309 resizeCanvas(); 310 311 // Mouse tracking 312 document.addEventListener('mousemove', handleMouseMove); 313 window.addEventListener('resize', resizeCanvas); 314 315 readTrailAmountFromSettings(); 316 readTrailColorsFromSettings(); 317 document.addEventListener('pupwife:setting-changed', handleSettingChanged); 318 requestTrailExtensions(); 319 320 emitDevLog('⨠Cursor trails initialized!'); 321 } 322 323 function normalizeTrailAmount(value) { 324 const n = parseInt(value, 10); 325 if (Number.isNaN(n)) return 2; 326 return Math.max(0, Math.min(4, n)); 327 } 328 329 function readTrailAmountFromSettings() { 330 try { 331 const saved = localStorage.getItem('puppywifey_settings'); 332 if (saved) { 333 const parsed = JSON.parse(saved); 334 if (parsed.trailAmount !== undefined) { 335 setTrailAmount(parsed.trailAmount); 336 } 337 } 338 } catch (e) { /* ignore */ } 339 } 340 341 function handleSettingChanged(e) { 342 if (!e || !e.detail) return; 343 if (e.detail.key === 'trailAmount') { 344 setTrailAmount(e.detail.value); 345 } else if (e.detail.key === 'trailColors') { 346 setTrailColors(e.detail.value); 347 } 348 } 349 350 function resizeCanvas() { 351 if (!canvas) return; 352 canvas.width = window.innerWidth; 353 canvas.height = window.innerHeight; 354 } 355 356 function handleMouseMove(e) { 357 const newX = e.clientX; 358 const newY = e.clientY; 359 360 // Calculate velocity 361 mouseVelX = newX - lastMouseX; 362 mouseVelY = newY - lastMouseY; 363 364 lastMouseX = newX; 365 lastMouseY = newY; 366 367 // Spawn particles based on trail type 368 if (currentTrailType !== 'none') { 369 spawnParticles(newX, newY); 370 } 371 } 372 373 // ==================================== 374 // PARTICLE SPAWNING 375 // ==================================== 376 377 function spawnParticles(x, y) { 378 const preset = TRAIL_AMOUNT_PRESETS[currentTrailAmount] || TRAIL_AMOUNT_PRESETS[2]; 379 const speed = Math.sqrt(mouseVelX * mouseVelX + mouseVelY * mouseVelY); 380 381 if (speed < preset.speedMin) return; 382 if (particles.length >= preset.maxParticles) return; 383 if (Math.random() > preset.spawnChance) return; 384 385 for (let b = 0; b < preset.burst; b++) { 386 if (b > 0 && Math.random() > 0.55) continue; 387 const sx = x + (b > 0 ? (Math.random() - 0.5) * 12 : 0); 388 const sy = y + (b > 0 ? (Math.random() - 0.5) * 12 : 0); 389 spawnTrailParticle(sx, sy); 390 } 391 } 392 393 function spawnTrailParticle(x, y) { 394 if (isMatterTrail(currentTrailType)) { 395 if (window.PupwifeCursorTrailsMatter) { 396 window.PupwifeCursorTrailsMatter.spawn(currentTrailType, x, y, { 397 vx: mouseVelX, 398 vy: mouseVelY 399 }); 400 } 401 return; 402 } 403 if (isHandlerTrail(currentTrailType)) { 404 if (window.PupwifeCursorTrailHandlers) { 405 window.PupwifeCursorTrailHandlers.spawn(currentTrailType, x, y, getHandlerCtx()); 406 } 407 return; 408 } 409 410 const lenBefore = particles.length; 411 412 switch (currentTrailType) { 413 case 'sparkles': spawnSparkle(x, y); break; 414 case 'fairy': spawnFairy(x, y); break; 415 case 'rainbow': spawnRainbow(x, y); break; 416 case 'bubbles': spawnBubble(x, y); break; 417 case 'hearts': spawnHeart(x, y); break; 418 case 'stars': spawnStar(x, y); break; 419 case 'fire': spawnFire(x, y); break; 420 case 'snow': spawnSnow(x, y); break; 421 case 'leaves': spawnLeaf(x, y); break; 422 // NEW TRAILS 423 case 'lightning': spawnLightning(x, y); break; 424 case 'cherryBlossoms': spawnCherryBlossom(x, y); break; 425 case 'fireflies': spawnFirefly(x, y); break; 426 case 'butterflies': spawnButterfly(x, y); break; 427 case 'comets': spawnComet(x, y); break; 428 case 'magicRunes': spawnMagicRune(x, y); break; 429 case 'crystals': spawnCrystal(x, y); break; 430 case 'energyOrbs': spawnEnergyOrb(x, y); break; 431 case 'matrix': spawnMatrix(x, y); break; 432 case 'neonLines': spawnNeonLine(x, y); break; 433 case 'glitch': spawnGlitch(x, y); break; 434 case 'musicNotes': spawnMusicNote(x, y); break; 435 case 'confetti': spawnConfetti(x, y); break; 436 case 'paintSplatter': spawnPaintSplatter(x, y); break; 437 case 'smoke': spawnSmoke(x, y); break; 438 case 'ripples': spawnRipple(x, y); break; 439 case 'dnaHelix': spawnDNA(x, y); break; 440 case 'circuit': spawnCircuit(x, y); break; 441 case 'binary': spawnBinary(x, y); break; 442 case 'pawPrints': spawnPawPrint(x, y); break; 443 // Heart colors 444 case 'orangeHearts': spawnColoredHeart(x, y, 'ð§¡'); break; 445 case 'yellowHearts': spawnColoredHeart(x, y, 'ð'); break; 446 case 'blueHearts': spawnColoredHeart(x, y, 'ð'); break; 447 case 'redHearts': spawnColoredHeart(x, y, 'â¤ï¸'); break; 448 case 'purpleHearts': spawnColoredHeart(x, y, 'ð'); break; 449 case 'greenHearts': spawnColoredHeart(x, y, 'ð'); break; 450 case 'whiteHearts': spawnColoredHeart(x, y, 'ð¤'); break; 451 case 'blackHearts': spawnColoredHeart(x, y, 'ð¤'); break;
452 case 'rainbowHearts': spawnRainbowHeart(x, y); break; 453 case 'dandelion': spawnDandelionSeed(x, y); break; 454 case 'rosePetals': spawnRosePetal(x, y); break; 455 case 'pixelated': spawnPixel(x, y); break; 456 case 'galaxy': spawnGalaxy(x, y); break; 457 case 'auroraWaves': spawnAuroraWave(x, y); break; 458 case 'geometric': spawnGeometric(x, y); break; 459 case 'spiral': spawnSpiral(x, y); break; 460 case 'inkBlots': spawnInkBlot(x, y); break; 461 case 'paintStrokes': spawnPaintStroke(x, y); break; 462 case 'laserBeams': spawnLaser(x, y); break; 463 case 'holograms': spawnHologram(x, y); break; 464 case 'clouds': spawnCloud(x, y); break; 465 case 'tornado': spawnTornado(x, y); break; 466 case 'emojis': spawnEmoji(x, y); break; 467 case 'candy': spawnCandy(x, y); break; 468 case 'electricSparks': spawnElectricSpark(x, y); break; 469 case 'plasma': spawnPlasma(x, y); break; 470 case 'vortex': spawnVortex(x, y); break; 471 case 'cosmicDust': spawnCosmicDust(x, y); break; 472 // NEW PHYSICS & OCEAN TRAILS 473 case 'bouncyBalls': spawnBouncyBall(x, y); break; 474 case 'physicsBoxes': spawnPhysicsBox(x, y); break; 475 case 'jellyfish': spawnJellyfish(x, y); break; 476 case 'fish': spawnFish(x, y); break; 477 case 'acorns': spawnAcorn(x, y); break; 478 case 'pinecones': spawnPinecone(x, y); break; 479 case 'raindrops': spawnRaindrop(x, y); break; 480 case 'mist': spawnMist(x, y); break; 481 case 'fog': spawnFog(x, y); break; 482 case 'chalk': spawnChalk(x, y); break; 483 case 'pastels': spawnPastel(x, y); break; 484 case 'dataPackets': spawnDataPacket(x, y); break; 485 case 'soundWaves': spawnSoundWave(x, y); break; 486 case 'trafficLights': spawnTrafficLight(x, y); break; 487 case 'bees': spawnBee(x, y); break; 488 case 'lensFlares': spawnLensFlare(x, y); break; 489 case 'prisms': spawnPrism(x, y); break; 490 case 'chainLinks': spawnChainLink(x, y); break; 491 case 'dominoes': spawnDomino(x, y); break; 492 case 'seahorses': spawnSeahorse(x, y); break; 493 case 'starfish': spawnStarfish(x, y); break; 494 case 'coral': spawnCoral(x, y); break; 495 case 'mushrooms': spawnMushroom(x, y); break; 496 case 'feathers': spawnFeather(x, y); break; 497 case 'fruit': spawnFruit(x, y); break; 498 case 'portals': spawnPortal(x, y); break; 499 case 'manaOrbs': spawnManaOrb(x, y); break; 500 case 'vinylRecords': spawnVinylRecord(x, y); break; 501 case 'coffeeBeans': spawnCoffeeBean(x, y); break; 502 case 'sprinkles': spawnSprinkle(x, y); break; 503 case 'dice': spawnDice(x, y); break; 504 case 'chatBubbles': spawnChatBubble(x, y); break; 505 case 'spaghetti': spawnSpaghetti(x, y); break; 506 } 507 508 for (let i = lenBefore; i < particles.length; i++) { 509 tintParticleIfSupported(particles[i], i - lenBefore); 510 } 511 } 512 513 function findLastParticleOfType(type) { 514 for (let i = particles.length - 1; i >= 0; i--) { 515 if (particles[i].type === type && particles[i].life > 0.15) { 516 return particles[i]; 517 } 518 } 519 return null; 520 } 521 522 // ==================================== 523 // PARTICLE TYPES 524 // ==================================== 525 526 function spawnSparkle(x, y) { 527 particles.push({ 528 x: x + (Math.random() - 0.5) * 10, 529 y: y + (Math.random() - 0.5) * 10, 530 vx: (Math.random() - 0.5) * 2, 531 vy: (Math.random() - 0.5) * 2, 532 life: 1, 533 maxLife: 0.5 + Math.random() * 0.5, 534 size: 2 + Math.random() * 4, 535 color: `hsl(${Math.random() * 360}, 100%, 70%)`, 536 type: 'sparkle' 537 }); 538 } 539 540 function spawnFairy(x, y) { 541 particles.push({ 542 x: x + (Math.random() - 0.5) * 15, 543 y: y + (Math.random() - 0.5) * 15, 544 vx: (Math.random() - 0.5) * 1, 545 vy: -1 - Math.random() * 2, 546 life: 1, 547 maxLife: 1 + Math.random() * 1, 548 size: 3 + Math.random() * 3, 549 color: `hsl(${280 + Math.random() * 60}, 100%, 80%)`, 550 glow: 15, 551 type: 'fairy' 552 }); 553 } 554 555 function spawnRainbow(x, y) { 556 const hue = (Date.now() / 10) % 360; 557 particles.push({ 558 x: x + (Math.random() - 0.5) * 8, 559 y: y + (Math.random() - 0.5) * 8, 560 vx: (Math.random() - 0.5) * 1.5, 561 vy: (Math.random() - 0.5) * 1.5, 562 life: 1, 563 maxLife: 0.8 + Math.random() * 0.4, 564 size: 4 + Math.random() * 4, 565 color: `hsl(${hue}, 100%, 60%)`, 566 type: 'rainbow' 567 }); 568 } 569 570 function spawnBubble(x, y) { 571 particles.push({ 572 x: x + (Math.random() - 0.5) * 12, 573 y: y + (Math.random() - 0.5) * 12, 574 vx: (Math.random() - 0.5) * 0.5, 575 vy: -0.5 - Math.random() * 1.5, 576 life: 1, 577 maxLife: 1.5 + Math.random() * 1, 578 size: 5 + Math.random() * 10, 579 rotation: Math.random() * Math.PI * 2, 580 rotationSpeed: (Math.random() - 0.5) * 0.1, 581 type: 'bubble' 582 }); 583 } 584 585 function spawnHeart(x, y) { 586 if (Math.random() > 0.7) { // Spawn less frequently 587 particles.push({ 588 x: x + (Math.random() - 0.5) * 10, 589 y: y + (Math.random() - 0.5) * 10, 590 vx: (Math.random() - 0.5) * 0.8, 591 vy: -0.5 - Math.random() * 1, 592 life: 1, 593 maxLife: 1.2 + Math.random() * 0.8, 594 size: 10 + Math.random() * 8, 595 rotation: (Math.random() - 0.5) * 0.5, 596 rotationSpeed: (Math.random() - 0.5) * 0.05, 597 type: 'heart' 598 }); 599 } 600 } 601 602 function spawnStar(x, y) { 603 particles.push({ 604 x: x + (Math.random() - 0.5) * 12, 605 y: y + (Math.random() - 0.5) * 12, 606 vx: (Math.random() - 0.5) * 2, 607 vy: (Math.random() - 0.5) * 2, 608 life: 1, 609 maxLife: 0.6 + Math.random() * 0.6, 610 size: 8 + Math.random() * 8, 611 rotation: Math.random() * Math.PI * 2, 612 rotationSpeed: (Math.random() - 0.5) * 0.2, 613 points: 5, 614 type: 'star' 615 }); 616 } 617 618 function spawnFire(x, y) { 619 particles.push({ 620 x: x + (Math.random() - 0.5) * 10, 621 y: y + Math.random() * 10, 622 vx: (Math.random() - 0.5) * 1, 623 vy: -2 - Math.random() * 3, 624 life: 1, 625 maxLife: 0.4 + Math.random() * 0.4, 626 size: 8 + Math.random() * 12, 627 color: Math.random() > 0.5 ? '#ff6b00' : '#ff0000', 628 type: 'fire' 629 }); 630 } 631 632 function spawnSnow(x, y) { 633 particles.push({ 634 x: x + (Math.random() - 0.5) * 15, 635 y: y + (Math.random() - 0.5) * 15, 636 vx: (Math.random() - 0.5) * 0.5, 637 vy: 0.5 + Math.random() * 1.5, 638 life: 1, 639 maxLife: 1.5 + Math.random() * 1, 640 size: 3 + Math.random() * 5, 641 rotation: Math.random() * Math.PI * 2, 642 rotationSpeed: (Math.random() - 0.5) * 0.05, 643 type: 'snow' 644 }); 645 } 646 647 function spawnLeaf(x, y) { 648 if (Math.random() > 0.6) { // Spawn less frequently 649 particles.push({ 650 x: x + (Math.random() - 0.5) * 15, 651 y: y + (Math.random() - 0.5) * 15, 652 vx: (Math.random() - 0.5) * 2, 653 vy: 1 + Math.random() * 2, 654 life: 1, 655 maxLife: 2 + Math.random() * 1, 656 size: 8 + Math.random() * 8, 657 rotation: Math.random() * Math.PI * 2, 658 rotationSpeed: (Math.random() - 0.5) * 0.15, 659 color: ['#ff6b00', '#ff8c00', '#ffa500', '#8b4513', '#a0522d'][Math.floor(Math.random() * 5)], 660 type: 'leaf' 661 }); 662 } 663 } 664 665 // ==================================== 666 // NEW PARTICLE TYPES (40 MORE!) 667 // ==================================== 668 669 function spawnLightning(x, y) { 670 particles.push({ 671 x: x + (Math.random() - 0.5) * 30, 672 y: y + (Math.random() - 0.5) * 30, 673 vx: 0, 674 vy: 0, 675 life: 1, 676 maxLife: 0.2, 677 segments: Array.from({length: 5}, () => ({ 678 x: (Math.random() - 0.5) * 20, 679 y: Math.random() * 30 680 })), 681 color: ['#fff', '#c0f', '#0ff'][Math.floor(Math.random() * 3)], 682 type: 'lightning' 683 }); 684 } 685 686 function spawnCherryBlossom(x, y) { 687 particles.push({ 688 x: x + (Math.random() - 0.5) * 10, 689 y: y + (Math.random() - 0.5) * 10, 690 vx: (Math.random() - 0.5) * 1.5, 691 vy: 0.5 + Math.random() * 1, 692 life: 1, 693 maxLife: 2 + Math.random(), 694 size: 6 + Math.random() * 4, 695 rotation: Math.random() * Math.PI * 2, 696 rotationSpeed: (Math.random() - 0.5) * 0.1, 697 wobble: Math.random() * Math.PI * 2, 698 type: 'cherryBlossom' 699 }); 700 } 701 702 function spawnFirefly(x, y) { 703 particles.push({ 704 x: x + (Math.random() - 0.5) * 20, 705 y: y + (Math.random() - 0.5) * 20, 706 vx: (Math.random() - 0.5) * 0.5, 707 vy: (Math.random() - 0.5) * 0.5, 708 life: 1, 709 maxLife: 2 + Math.random() * 2, 710 size: 3 + Math.random() * 2, 711 glow: 20, 712 pulse: Math.random() * Math.PI * 2, 713 color: ['#ffeb3b', '#76ff03'][Math.floor(Math.random() * 2)], 714 type: 'firefly' 715 }); 716 } 717 718 function spawnButterfly(x, y) { 719 if (Math.random() > 0.7) { 720 particles.push({ 721 x: x + (Math.random() - 0.5) * 10, 722 y: y + (Math.random() - 0.5) * 10, 723 vx: (Math.random() - 0.5) * 2, 724 vy: -1 - Math.random(), 725 life: 1, 726 maxLife: 3 + Math.random() * 2, 727 size: 10 + Math.random() * 5, 728 wingFlap: 0, 729 color: ['#ff69b4', '#9c27b0', '#00bcd4', '#ffeb3b'][Math.floor(Math.random() * 4)], 730 type: 'butterfly' 731 }); 732 } 733 } 734 735 function spawnComet(x, y) { 736 const angle = Math.random() * Math.PI * 2; 737 particles.push({ 738 x: x, 739 y: y, 740 vx: Math.cos(angle) * 5, 741 vy: Math.sin(angle) * 5, 742 life: 1, 743 maxLife: 0.6, 744 size: 4 + Math.random() * 3, 745 tail: [], 746 color: ['#fff', '#ff0', '#0ff'][Math.floor(Math.random() * 3)], 747 type: 'comet' 748 }); 749 } 750 751 function spawnMagicRune(x, y) { 752 if (Math.random() > 0.5) { 753 particles.push({ 754 x: x + (Math.random() - 0.5) * 10, 755 y: y + (Math.random() - 0.5) * 10, 756 vx: 0, 757 vy: -0.5 - Math.random() * 0.5, 758 life: 1, 759 maxLife: 2, 760 size: 15 + Math.random() * 10, 761 rotation: Math.random() * Math.PI * 2, 762 rotationSpeed: (Math.random() - 0.5) * 0.1, 763 rune: ['á ', 'á¢', 'á¦', 'á¨', 'á±', 'á²'][Math.floor(Math.random() * 6)], 764 color: `hsl(${270 + Math.random() * 60}, 100%, 70%)`, 765 type: 'magicRune' 766 }); 767 } 768 } 769 770 function spawnCrystal(x, y) { 771 particles.push({ 772 x: x + (Math.random() - 0.5) * 10, 773 y: y + (Math.random() - 0.5) * 10, 774 vx: (Math.random() - 0.5) * 1, 775 vy: (Math.random() - 0.5) * 1, 776 life: 1, 777 maxLife: 1.5, 778 size: 8 + Math.random() * 6, 779 rotation: Math.random() * Math.PI * 2, 780 rotationSpeed: (Math.random() - 0.5) * 0.15, 781 sides: Math.floor(3 + Math.random() * 3), 782 color: `hsl(${Math.random() * 360}, 100%, 70%)`, 783 type: 'crystal' 784 }); 785 } 786 787 function spawnEnergyOrb(x, y) { 788 particles.push({ 789 x: x + (Math.random() - 0.5) * 15, 790 y: y + (Math.random() - 0.5) * 15, 791 vx: (Math.random() - 0.5) * 2, 792 vy: (Math.random() - 0.5) * 2, 793 life: 1, 794 maxLife: 1 + Math.random(), 795 size: 8 + Math.random() * 8, 796 glow: 25, 797 pulse: Math.random() * Math.PI * 2, 798 color: `hsl(${180 + Math.random() * 60}, 100%, 60%)`, 799 type: 'energyOrb' 800 }); 801 } 802 803 function spawnMatrix(x, y) { 804 particles.push({ 805 x: x + (Math.random() - 0.5) * 20, 806 y: y - 20, 807 vx: 0, 808 vy: 3 + Math.random() * 2, 809 life: 1, 810 maxLife: 1, 811 char: String.fromCharCode(0x30A0 + Math.random() * 96), 812 size: 12 + Math.random() * 6, 813 type: 'matrix' 814 }); 815 } 816 817 function spawnNeonLine(x, y) { 818 const angle = Math.atan2(mouseVelY, mouseVelX); 819 particles.push({ 820 x: x, 821 y: y, 822 vx: 0, 823 vy: 0, 824 angle: angle, 825 length: 20 + Math.random() * 30, 826 life: 1, 827 maxLife: 0.4, 828 color: `hsl(${Math.random() * 360}, 100%, 60%)`, 829 glow: 15, 830 type: 'neonLine' 831 }); 832 } 833 834 function spawnGlitch(x, y) { 835 particles.push({ 836 x: x + (Math.random() - 0.5) * 30, 837 y: y + (Math.random() - 0.5) * 30, 838 vx: (Math.random() - 0.5) * 5, 839 vy: (Math.random() - 0.5) * 5, 840 life: 1, 841 maxLife: 0.3, 842 width: 10 + Math.random() * 30, 843 height: 2 + Math.random() * 10, 844 color: ['#f00', '#0f0', '#00f', '#f0f', '#0ff'][Math.floor(Math.random() * 5)], 845 type: 'glitch' 846 }); 847 } 848 849 function spawnMusicNote(x, y) { 850 if (Math.random() > 0.6) { 851 particles.push({ 852 x: x + (Math.random() - 0.5) * 10, 853 y: y + (Math.random() - 0.5) * 10, 854 vx: (Math.random() - 0.5) * 0.5, 855 vy: -1 - Math.random(), 856 life: 1, 857 maxLife: 2, 858 size: 12 + Math.random() * 6, 859 note: ['âª', 'â«', 'â¬', 'â©'][Math.floor(Math.random() * 4)], 860 color: `hsl(${Math.random() * 360}, 70%, 60%)`, 861 type: 'musicNote' 862 }); 863 } 864 } 865 866 function spawnConfetti(x, y) { 867 particles.push({ 868 x: x + (Math.random() - 0.5) * 10, 869 y: y + (Math.random() - 0.5) * 10, 870 vx: (Math.random() - 0.5) * 3, 871 vy: -2 - Math.random() * 2, 872 life: 1, 873 maxLife: 1.5, 874 size: 4 + Math.random() * 6, 875 rotation: Math.random() * Math.PI * 2, 876 rotationSpeed: (Math.random() - 0.5) * 0.3, 877 shape: ['rect', 'circle'][Math.floor(Math.random() * 2)], 878 color: `hsl(${Math.random() * 360}, 100%, 60%)`, 879 type: 'confetti' 880 }); 881 } 882 883 function spawnPaintSplatter(x, y) { 884 particles.push({ 885 x: x + (Math.random() - 0.5) * 15, 886 y: y + (Math.random() - 0.5) * 15, 887 vx: (Math.random() - 0.5) * 2, 888 vy: (Math.random() - 0.5) * 2, 889 life: 1, 890 maxLife: 1, 891 size: 10 + Math.random() * 15, 892 splats: Array.from({length: 5}, () => ({ 893 angle: Math.random() * Math.PI * 2, 894 length: Math.random() * 10 895 })), 896 color: `hsl(${Math.random() * 360}, 80%, 55%)`, 897 type: 'paintSplatter' 898 }); 899 } 900 901 function spawnSmoke(x, y) { 902 particles.push({ 903 x: x + (Math.random() - 0.5) * 10, 904 y: y + (Math.random() - 0.5) * 10, 905 vx: (Math.random() - 0.5) * 0.5, 906 vy: -0.5 - Math.random() * 0.5, 907 life: 1, 908
908 maxLife: 2, 909 size: 15 + Math.random() * 20, 910 expansion: 1, 911 type: 'smoke' 912 }); 913 } 914 915 function spawnRipple(x, y) { 916 if (Math.random() > 0.5) { // Made more frequent 917 particles.push({ 918 x: x, 919 y: y, 920 vx: 0, 921 vy: 0, 922 life: 1, 923 maxLife: 1, 924 size: 5, 925 maxSize: 50 + Math.random() * 50, 926 type: 'ripple' 927 }); 928 } 929 } 930 931 function spawnDNA(x, y) { 932 particles.push({ 933 x: x + (Math.random() - 0.5) * 10, 934 y: y + (Math.random() - 0.5) * 10, 935 vx: 0, 936 vy: -1, 937 life: 1, 938 maxLife: 2, 939 size: 8, 940 twist: Math.random() * Math.PI * 2, 941 type: 'dna' 942 }); 943 } 944 945 function spawnCircuit(x, y) { 946 particles.push({ 947 x: x + (Math.random() - 0.5) * 10, 948 y: y + (Math.random() - 0.5) * 10, 949 vx: (Math.random() - 0.5) * 1, 950 vy: (Math.random() - 0.5) * 1, 951 life: 1, 952 maxLife: 1.5, 953 size: 8 + Math.random() * 8, 954 lines: Array.from({length: 4}, () => ({ 955 angle: Math.random() * Math.PI * 2, 956 length: 5 + Math.random() * 10 957 })), 958 type: 'circuit' 959 }); 960 } 961 962 function spawnBinary(x, y) { 963 particles.push({ 964 x: x + (Math.random() - 0.5) * 20, 965 y: y - 10, 966 vx: 0, 967 vy: 2 + Math.random(), 968 life: 1, 969 maxLife: 1.5, 970 digit: Math.random() > 0.5 ? '1' : '0', 971 size: 10 + Math.random() * 4, 972 type: 'binary' 973 }); 974 } 975 976 function spawnPawPrint(x, y) { 977 if (Math.random() > 0.7) { // 30% spawn rate - less frequent to avoid solid lines 978 // Load paw print image if not already loaded 979 if (!pawPrintImage) { 980 pawPrintImage = new Image(); 981 pawPrintImage.onload = () => { 982 pawPrintImageLoaded = true; 983 emitDevLog('â Paw print image loaded successfully'); 984 }; 985 pawPrintImage.onerror = () => { 986 emitDevWarn('â Failed to load paw print image from /images/paw.png, using fallback'); 987 pawPrintImageLoaded = false; 988 }; 989 pawPrintImage.src = '/images/paw.png'; 990 } 991 992 particles.push({ 993 x: x, 994 y: y, 995 vx: 0, 996 vy: 0, 997 life: 1, 998 maxLife: 2, 999 size: 15 + Math.random() * 5, 1000 rotation: Math.random() * Math.PI * 2, 1001 color: ['#ff69b4', '#9c27b0', '#00bcd4'][Math.floor(Math.random() * 3)], 1002 type: 'pawPrint' 1003 }); 1004 } 1005 } 1006 1007 // ==================================== 1008 // COLORED HEART TRAILS 1009 // ==================================== 1010 1011 function spawnColoredHeart(x, y, emoji) { 1012 if (Math.random() > 0.6) { // Spawn less frequently 1013 particles.push({ 1014 x: x + (Math.random() - 0.5) * 10, 1015 y: y + (Math.random() - 0.5) * 10, 1016 vx: (Math.random() - 0.5) * 0.8, 1017 vy: -0.5 - Math.random() * 1, 1018 life: 1, 1019 maxLife: 1.2 + Math.random() * 0.8, 1020 size: 16 + Math.random() * 8, 1021 rotation: (Math.random() - 0.5) * 0.5, 1022 rotationSpeed: (Math.random() - 0.5) * 0.05, 1023 emoji: emoji, 1024 type: 'coloredHeart' 1025 }); 1026 } 1027 } 1028 1029 function spawnRainbowHeart(x, y) { 1030 if (Math.random() > 0.6) { // Spawn less frequently 1031 const hearts = ['â¤ï¸', 'ð§¡', 'ð', 'ð', 'ð', 'ð']; 1032 const emoji = hearts[Math.floor(Math.random() * hearts.length)]; 1033 particles.push({ 1034 x: x + (Math.random() - 0.5) * 10, 1035 y: y + (Math.random() - 0.5) * 10, 1036 vx: (Math.random() - 0.5) * 0.8, 1037 vy: -0.5 - Math.random() * 1, 1038 life: 1, 1039 maxLife: 1.2 + Math.random() * 0.8, 1040 size: 16 + Math.random() * 8, 1041 rotation: (Math.random() - 0.5) * 0.5, 1042 rotationSpeed: (Math.random() - 0.5) * 0.05, 1043 emoji: emoji, 1044 type: 'coloredHeart' 1045 }); 1046 } 1047 } 1048 1049 function spawnDandelionSeed(x, y) { 1050 particles.push({ 1051 x: x + (Math.random() - 0.5) * 10, 1052 y: y + (Math.random() - 0.5) * 10, 1053 vx: (Math.random() - 0.5) * 0.5, 1054 vy: -0.3 - Math.random() * 0.5, 1055 life: 1, 1056 maxLife: 3 + Math.random() * 2, 1057 size: 8 + Math.random() * 4, 1058 rotation: Math.random() * Math.PI * 2, 1059 rotationSpeed: (Math.random() - 0.5) * 0.05, 1060 type: 'dandelion' 1061 }); 1062 } 1063 1064 function spawnRosePetal(x, y) { 1065 particles.push({ 1066 x: x + (Math.random() - 0.5) * 10, 1067 y: y + (Math.random() - 0.5) * 10, 1068 vx: (Math.random() - 0.5) * 1, 1069 vy: 0.5 + Math.random(), 1070 life: 1, 1071 maxLife: 2 + Math.random(), 1072 size: 8 + Math.random() * 6, 1073 rotation: Math.random() * Math.PI * 2, 1074 rotationSpeed: (Math.random() - 0.5) * 0.1, 1075 wobble: Math.random() * Math.PI * 2, 1076 color: ['#ff1744', '#f50057', '#ff69b4', '#ff80ab'][Math.floor(Math.random() * 4)], 1077 type: 'rosePetal' 1078 }); 1079 } 1080 1081 function spawnPixel(x, y) { 1082 particles.push({ 1083 x: Math.floor(x / 8) * 8, 1084 y: Math.floor(y / 8) * 8, 1085 vx: (Math.random() - 0.5) * 2, 1086 vy: (Math.random() - 0.5) * 2, 1087 life: 1, 1088 maxLife: 0.8, 1089 size: 8, 1090 color: `hsl(${Math.random() * 360}, 80%, 60%)`, 1091 type: 'pixelated' 1092 }); 1093 } 1094 1095 function spawnGalaxy(x, y) { 1096 particles.push({ 1097 x: x + (Math.random() - 0.5) * 20, 1098 y: y + (Math.random() - 0.5) * 20, 1099 vx: (Math.random() - 0.5) * 0.5, 1100 vy: (Math.random() - 0.5) * 0.5, 1101 life: 1, 1102 maxLife: 2 + Math.random(), 1103 size: 3 + Math.random() * 5, 1104 orbitAngle: Math.random() * Math.PI * 2, 1105 orbitSpeed: (Math.random() - 0.5) * 0.1, 1106 color: ['#fff', '#c9f', '#9cf', '#fc9'][Math.floor(Math.random() * 4)], 1107 type: 'galaxy' 1108 }); 1109 } 1110 1111 function spawnAuroraWave(x, y) { 1112 particles.push({ 1113 x: x, 1114 y: y + (Math.random() - 0.5) * 20, 1115 vx: (Math.random() - 0.5) * 2, 1116 vy: -0.5, 1117 life: 1, 1118 maxLife: 2, 1119 width: 30 + Math.random() * 40, 1120 height: 20 + Math.random() * 20, 1121 wave: Math.random() * Math.PI * 2, 1122 color: `hsl(${150 + Math.random() * 80}, 80%, 60%)`, 1123 type: 'auroraWave' 1124 }); 1125 } 1126 1127 function spawnGeometric(x, y) { 1128 particles.push({ 1129 x: x + (Math.random() - 0.5) * 10, 1130 y: y + (Math.random() - 0.5) * 10, 1131 vx: (Math.random() - 0.5) * 1.5, 1132 vy: (Math.random() - 0.5) * 1.5, 1133 life: 1, 1134 maxLife: 1.5, 1135 size: 10 + Math.random() * 10, 1136 rotation: Math.random() * Math.PI * 2, 1137 rotationSpeed: (Math.random() - 0.5) * 0.2, 1138 sides: Math.floor(3 + Math.random() * 5), 1139 color: `hsl(${Math.random() * 360}, 70%, 60%)`, 1140 type: 'geometric' 1141 }); 1142 } 1143 1144 function spawnSpiral(x, y) { 1145 particles.push({ 1146 x: x, 1147 y: y, 1148 centerX: x, 1149 centerY: y, 1150 vx: 0, 1151 vy: 0, 1152 angle: 0, 1153 radius: 0, 1154 angleSpeed: 0.3, 1155 radiusSpeed: 2, 1156 life: 1, 1157 maxLife: 1.5, 1158 color: `hsl(${Math.random() * 360}, 80%, 60%)`, 1159 type: 'spiral' 1160 }); 1161 } 1162 1163 function spawnInkBlot(x, y) { 1164 particles.push({ 1165 x: x + (Math.random() - 0.5) * 10, 1166 y: y + (Math.random() - 0.5) * 10, 1167 vx: (Math.random() - 0.5) * 1, 1168 vy: (Math.random() - 0.5) * 1, 1169 life: 1, 1170 maxLife: 1.5, 1171 size: 15 + Math.random() * 15, 1172 expansion: 1, 1173 blobs: Array.from({length: 3}, () => ({ 1174 x: (Math.random() - 0.5) * 10, 1175 y: (Math.random() - 0.5) * 10, 1176 size: 5 + Math.random() * 10 1177 })), 1178 color: ['#000', '#1a237e', '#4a148c'][Math.floor(Math.random() * 3)], 1179 type: 'inkBlot' 1180 }); 1181 } 1182 1183 function spawnPaintStroke(x, y) { 1184 const angle = Math.atan2(mouseVelY, mouseVelX); 1185 particles.push({ 1186 x: x, 1187 y: y, 1188 vx: 0, 1189 vy: 0, 1190 angle: angle, 1191 length: 20 + Math.random() * 30, 1192 width: 5 + Math.random() * 10, 1193 life: 1, 1194 maxLife: 0.8, 1195 color: `hsl(${Math.random() * 360}, 70%, 55%)`, 1196 type: 'paintStroke' 1197 }); 1198 } 1199 1200 function spawnLaser(x, y) { 1201 const angle = Math.atan2(mouseVelY, mouseVelX); 1202 particles.push({ 1203 x: x, 1204 y: y, 1205 vx: 0, 1206 vy: 0, 1207 angle: angle, 1208 length: 40 + Math.random() * 60, 1209 life: 1, 1210 maxLife: 0.3, 1211 color: ['#f00', '#0f0', '#00f', '#f0f', '#0ff'][Math.floor(Math.random() * 5)], 1212 glow: 20, 1213 type: 'laser' 1214 }); 1215 } 1216 1217 function spawnHologram(x, y) { 1218 particles.push({ 1219 x: x + (Math.random() - 0.5) * 15, 1220 y: y + (Math.random() - 0.5) * 15, 1221 vx: 0, 1222 vy: -0.5, 1223 life: 1, 1224 maxLife: 1.5, 1225 size: 10 + Math.random() * 10, 1226 scanLines: 3, 1227 color: `hsl(${180 + Math.random() * 40}, 100%, 60%)`, 1228 type: 'hologram' 1229 }); 1230 } 1231 1232 function spawnCloud(x, y) { 1233 particles.push({ 1234 x: x + (Math.random() - 0.5) * 20, 1235 y: y + (Math.random() - 0.5) * 20, 1236 vx: 0.5 + Math.random() * 0.5, 1237 vy: -0.2, 1238 life: 1, 1239 maxLife: 3 + Math.random() * 2, 1240 size: 20 + Math.random() * 20, 1241 puffs: Array.from({length: 3}, () => ({ 1242 x: (Math.random() - 0.5) * 15, 1243 y: (Math.random() - 0.5) * 8, 1244 size: 8 + Math.random() * 12 1245 })), 1246 type: 'cloud' 1247 }); 1248 } 1249 1250 function spawnTornado(x, y) { 1251 particles.push({ 1252 x: x, 1253 y: y, 1254 centerX: x + (Math.random() - 0.5) * 10, 1255 centerY: y, 1256 vx: 0, 1257 vy: -2, 1258 angle: Math.random() * Math.PI * 2, 1259 radius: 0, 1260 angleSpeed: 0.4, 1261 radiusSpeed: 1.5, 1262 life: 1, 1263 maxLife: 1.5, 1264 color: `rgba(150, 150, 150, ${0.5 + Math.random() * 0.3})`, 1265 type: 'tornado' 1266 }); 1267 } 1268 1269 function spawnEmoji(x, y) { 1270 if (Math.random() > 0.6) { 1271 particles.push({ 1272 x: x + (Math.random() - 0.5) * 10, 1273 y: y + (Math.random() - 0.5) * 10, 1274 vx: (Math.random() - 0.5) * 1, 1275 vy: -0.5 - Math.random() * 0.5, 1276 life: 1, 1277 maxLife: 2, 1278 size: 16 + Math.random() * 8, 1279 emoji: ['â¨', 'ð', 'ð', 'ð', 'ð¦', 'ð¸', 'ð«', 'ð¨'][Math.floor(Math.random() * 8)], 1280 rotation: (Math.random() - 0.5) * 0.5, 1281 rotationSpeed: (Math.random() - 0.5) * 0.05, 1282 type: 'emoji' 1283 }); 1284 } 1285 } 1286 1287 function spawnCandy(x, y) { 1288 if (Math.random() > 0.7) { 1289 particles.push({ 1290 x: x + (Math.random() - 0.5) * 10, 1291 y: y + (Math.random() - 0.5) * 10, 1292 vx: (Math.random() - 0.5) * 1, 1293 vy: (Math.random() - 0.5) * 1, 1294 life: 1, 1295 maxLife: 2, 1296 size: 10 + Math.random() * 8, 1297 rotation: Math.random() * Math.PI * 2, 1298 rotationSpeed: (Math.random() - 0.5) * 0.15, 1299 candyType: Math.floor(Math.random() * 3), // lollipop, wrapped candy, heart 1300 color: `hsl(${Math.random() * 360}, 100%, 65%)`, 1301 type: 'candy' 1302 }); 1303 } 1304 } 1305 1306 function spawnElectricSpark(x, y) { 1307 particles.push({ 1308 x: x + (Math.random() - 0.5) * 15, 1309 y: y + (Math.random() - 0.5) * 15, 1310 vx: (Math.random() - 0.5) * 3, 1311 vy: (Math.random() - 0.5) * 3, 1312 life: 1, 1313 maxLife: 0.4, 1314 size: 3 + Math.random() * 4, 1315 branches: Array.from({length: 3}, () => ({ 1316 angle: Math.random() * Math.PI * 2, 1317 length: 5 + Math.random() * 10 1318 })), 1319 color: ['#fff', '#ff0', '#0ff'][Math.floor(Math.random() * 3)], 1320 glow: 15, 1321 type: 'electricSpark' 1322 }); 1323 } 1324 1325 function spawnPlasma(x, y) { 1326 particles.push({ 1327 x: x + (Math.random() - 0.5) * 15, 1328 y: y + (Math.random() - 0.5) * 15, 1329 vx: (Math.random() - 0.5) * 1, 1330 vy: (Math.random() - 0.5) * 1, 1331 life: 1, 1332 maxLife: 1.5, 1333 size: 12 + Math.random() * 12, 1334 glow: 30, 1335 pulse: Math.random() * Math.PI * 2, 1336 color: `hsl(${280 + Math.random() * 40}, 100%, 60%)`, 1337 type: 'plasma' 1338 }); 1339 } 1340 1341 function spawnVortex(x, y) { 1342 particles.push({ 1343 x: x, 1344 y: y, 1345 centerX: x, 1346 centerY: y, 1347 vx: 0, 1348 vy: 0, 1349 angle: Math.random() * Math.PI * 2, 1350 radius: 5, 1351 angleSpeed: 0.5, 1352 radiusSpeed: -0.5, 1353 life: 1, 1354 maxLife: 1.5, 1355 size: 3, 1356 color: `hsl(${Math.random() * 360}, 80%, 60%)`, 1357 type: 'vortex' 1358 }); 1359 } 1360 1361 function spawnCosmicDust(x, y) { 1362 particles.push({ 1363 x: x + (Math.random() - 0.5) * 25, 1364 y: y + (Math.random() - 0.5) * 25, 1365 vx: (Math.random() - 0.5) * 0.5, 1366 vy: (Math.random() - 0.5) * 0.5, 1367 life: 1, 1368 maxLife: 2 + Math.random() * 2, 1369 size: 2 + Math.random() * 3, 1370 twinkle: Math.random() * Math.PI * 2, 1371 color: ['#fff', '#ffd', '#dff', '#fdf'][Math.floor(Math.random() * 4)], 1372 type: 'cosmicDust' 1373 }); 1374 } 1375 1376 // ==================================== 1377 // NEW PHYSICS & OCEAN TRAILS 1378 // ==================================== 1379 1380 function spawnBouncyBall(x, y) { 1381 particles.push({ 1382 x: x + (Math.random() - 0.5) * 10, 1383 y: y + (Math.random() - 0.5) * 10, 1384 vx: (Math.random() - 0.5) * 3, 1385 vy: (Math.random() - 0.5) * 3, 1386 life: 1, 1387 maxLife: 3 + Math.random() * 2, 1388 size: 8 + Math.random() * 8, 1389 color: `hsl(${Math.random() * 360}, 80%, 60%)`, 1390 bounce: 0.7, // Bounce coefficient 1391 gravity: 0.3, 1392 type: 'bouncyBall' 1393 }); 1394 } 1395 1396 function spawnPhysicsBox(x, y) { 1397 particles.push({ 1398 x: x + (Math.random() - 0.5) * 10, 1399 y: y + (Math.random() - 0.5) * 10, 1400 vx: (Math.random() - 0.5) * 2, 1401 vy: (Math.random() - 0.5) * 2, 1402 life: 1, 1403 maxLife: 3 + Math.random() * 2, 1404 size: 10 + Math.random() * 10, 1405 rotation: Math.random() * Math.PI * 2, 1406 rotationSpeed: (Math.random() - 0.5) * 0.2, 1407 bounce: 0.6, 1408 gravity: 0.25, 1409 color: `hsl(${Math.random() * 360}, 70%, 55%)`, 1410 type: 'physicsBox' 1411 }); 1412 } 1413 1414 function spawnJellyfish(x, y) { 1415 if (Math.random() > 0.5) { // Spawn less frequently 1416 const gifPath = Math.random() > 0.5 ? '/i
1416mages/fishes/jellyfish.gif' : '/images/fishes/jellyfishfast.gif'; 1417 const img = new Image(); 1418 img.src = gifPath; 1419 1420 particles.push({ 1421 x: x + (Math.random() - 0.5) * 15, 1422 y: y + (Math.random() - 0.5) * 15, 1423 vx: (Math.random() - 0.5) * 0.5, 1424 vy: -0.3 - Math.random() * 0.5, 1425 life: 1, 1426 maxLife: 4 + Math.random() * 2, 1427 size: 30 + Math.random() * 20, 1428 pulse: Math.random() * Math.PI * 2, 1429 pulseSpeed: 0.8 + Math.random() * 0.4, 1430 image: img, 1431 gifPath: gifPath, 1432 hue: Math.random() * 360, 1433 type: 'jellyfish' 1434 }); 1435 } 1436 } 1437 1438 function spawnFish(x, y) { 1439 if (Math.random() > 0.6) { 1440 particles.push({ 1441 x: x + (Math.random() - 0.5) * 10, 1442 y: y + (Math.random() - 0.5) * 10, 1443 vx: (Math.random() - 0.5) * 2, 1444 vy: (Math.random() - 0.5) * 1, 1445 life: 1, 1446 maxLife: 2 + Math.random() * 2, 1447 size: 12 + Math.random() * 8, 1448 tailWag: Math.random() * Math.PI * 2, 1449 direction: Math.random() > 0.5 ? 1 : -1, 1450 color: `hsl(${180 + Math.random() * 60}, 70%, 50%)`, 1451 type: 'fish' 1452 }); 1453 } 1454 } 1455 1456 function spawnAcorn(x, y) { 1457 if (Math.random() > 0.6) { 1458 particles.push({ 1459 x: x + (Math.random() - 0.5) * 10, 1460 y: y + (Math.random() - 0.5) * 10, 1461 vx: (Math.random() - 0.5) * 1, 1462 vy: 0.5 + Math.random() * 1.5, 1463 life: 1, 1464 maxLife: 2 + Math.random() * 1, 1465 size: 8 + Math.random() * 6, 1466 rotation: Math.random() * Math.PI * 2, 1467 rotationSpeed: (Math.random() - 0.5) * 0.1, 1468 gravity: 0.2, 1469 type: 'acorn' 1470 }); 1471 } 1472 } 1473 1474 function spawnPinecone(x, y) { 1475 if (Math.random() > 0.6) { 1476 particles.push({ 1477 x: x + (Math.random() - 0.5) * 10, 1478 y: y + (Math.random() - 0.5) * 10, 1479 vx: (Math.random() - 0.5) * 1, 1480 vy: 0.5 + Math.random() * 1.5, 1481 life: 1, 1482 maxLife: 2.5 + Math.random() * 1, 1483 size: 10 + Math.random() * 8, 1484 rotation: Math.random() * Math.PI * 2, 1485 rotationSpeed: (Math.random() - 0.5) * 0.15, 1486 gravity: 0.2, 1487 type: 'pinecone' 1488 }); 1489 } 1490 } 1491 1492 function spawnRaindrop(x, y) { 1493 particles.push({ 1494 x: x + (Math.random() - 0.5) * 5, 1495 y: y - 5, 1496 vx: (Math.random() - 0.5) * 0.3, 1497 vy: 2 + Math.random() * 2, 1498 life: 1, 1499 maxLife: 1.5, 1500 size: 2 + Math.random() * 3, 1501 splashY: null, // Will be set when hitting bottom 1502 type: 'raindrop' 1503 }); 1504 } 1505 1506 function spawnMist(x, y) { 1507 particles.push({ 1508 x: x + (Math.random() - 0.5) * 20, 1509 y: y + (Math.random() - 0.5) * 20, 1510 vx: (Math.random() - 0.5) * 0.3, 1511 vy: -0.2 - Math.random() * 0.3, 1512 life: 1, 1513 maxLife: 3 + Math.random() * 2, 1514 size: 20 + Math.random() * 30, 1515 expansion: 1, 1516 opacity: 0.3 + Math.random() * 0.2, 1517 type: 'mist' 1518 }); 1519 } 1520 1521 function spawnFog(x, y) { 1522 particles.push({ 1523 x: x + (Math.random() - 0.5) * 25, 1524 y: y + (Math.random() - 0.5) * 25, 1525 vx: (Math.random() - 0.5) * 0.2, 1526 vy: -0.1 - Math.random() * 0.2, 1527 life: 1, 1528 maxLife: 4 + Math.random() * 2, 1529 size: 30 + Math.random() * 40, 1530 expansion: 1, 1531 opacity: 0.2 + Math.random() * 0.15, 1532 type: 'fog' 1533 }); 1534 } 1535 1536 function spawnChalk(x, y) { 1537 particles.push({ 1538 x: x + (Math.random() - 0.5) * 8, 1539 y: y + (Math.random() - 0.5) * 8, 1540 vx: (Math.random() - 0.5) * 1.5, 1541 vy: (Math.random() - 0.5) * 1.5, 1542 life: 1, 1543 maxLife: 1.5 + Math.random() * 0.5, 1544 size: 3 + Math.random() * 4, 1545 smudge: 0, 1546 color: ['#fff', '#f0f0f0', '#e0e0e0', '#d0d0d0'][Math.floor(Math.random() * 4)], 1547 type: 'chalk' 1548 }); 1549 } 1550 1551 function spawnPastel(x, y) { 1552 particles.push({ 1553 x: x + (Math.random() - 0.5) * 10, 1554 y: y + (Math.random() - 0.5) * 10, 1555 vx: (Math.random() - 0.5) * 1, 1556 vy: (Math.random() - 0.5) * 1, 1557 life: 1, 1558 maxLife: 2 + Math.random(), 1559 size: 8 + Math.random() * 10, 1560 blend: 0, 1561 color: `hsl(${Math.random() * 360}, 40%, 75%)`, 1562 type: 'pastel' 1563 }); 1564 } 1565 1566 function spawnDataPacket(x, y) { 1567 particles.push({ 1568 x: x + (Math.random() - 0.5) * 15, 1569 y: y + (Math.random() - 0.5) * 15, 1570 vx: (Math.random() - 0.5) * 2, 1571 vy: (Math.random() - 0.5) * 2, 1572 life: 1, 1573 maxLife: 1.5 + Math.random(), 1574 size: 6 + Math.random() * 6, 1575 data: Math.random().toString(36).substring(2, 5).toUpperCase(), 1576 path: [], 1577 color: `hsl(${120 + Math.random() * 60}, 80%, 50%)`, 1578 type: 'dataPacket' 1579 }); 1580 } 1581 1582 function spawnSoundWave(x, y) { 1583 particles.push({ 1584 x: x, 1585 y: y, 1586 vx: 0, 1587 vy: 0, 1588 life: 1, 1589 maxLife: 1.5, 1590 size: 5, 1591 maxSize: 40 + Math.random() * 30, 1592 frequency: 0.1 + Math.random() * 0.1, 1593 phase: Math.random() * Math.PI * 2, 1594 color: `hsl(${200 + Math.random() * 60}, 70%, 60%)`, 1595 type: 'soundWave' 1596 }); 1597 } 1598 1599 function spawnTrafficLight(x, y) { 1600 if (Math.random() > 0.7) { 1601 particles.push({ 1602 x: x + (Math.random() - 0.5) * 10, 1603 y: y + (Math.random() - 0.5) * 10, 1604 vx: (Math.random() - 0.5) * 0.5, 1605 vy: -0.5 - Math.random() * 0.5, 1606 life: 1, 1607 maxLife: 3, 1608 size: 12 + Math.random() * 6, 1609 state: Math.floor(Math.random() * 3), // 0=red, 1=yellow, 2=green 1610 cycleTime: 0, 1611 type: 'trafficLight' 1612 }); 1613 } 1614 } 1615 1616 function spawnBee(x, y) { 1617 if (Math.random() > 0.6) { 1618 particles.push({ 1619 x: x + (Math.random() - 0.5) * 10, 1620 y: y + (Math.random() - 0.5) * 10, 1621 vx: (Math.random() - 0.5) * 2, 1622 vy: (Math.random() - 0.5) * 2, 1623 life: 1, 1624 maxLife: 2.5 + Math.random() * 1.5, 1625 size: 8 + Math.random() * 6, 1626 wingFlap: Math.random() * Math.PI * 2, 1627 zigzag: 0, 1628 color: '#ffeb3b', 1629 type: 'bee' 1630 }); 1631 } 1632 } 1633 1634 function spawnLensFlare(x, y) { 1635 particles.push({ 1636 x: x, 1637 y: y, 1638 vx: 0, 1639 vy: 0, 1640 life: 1, 1641 maxLife: 0.8, 1642 size: 20 + Math.random() * 30, 1643 angle: Math.random() * Math.PI * 2, 1644 rotation: Math.random() * Math.PI * 2, 1645 rotationSpeed: (Math.random() - 0.5) * 0.3, 1646 color: `rgba(255, 255, 255, ${0.6 + Math.random() * 0.3})`, 1647 type: 'lensFlare' 1648 }); 1649 } 1650 1651 function spawnPrism(x, y) { 1652 particles.push({ 1653 x: x + (Math.random() - 0.5) * 10, 1654 y: y + (Math.random() - 0.5) * 10, 1655 vx: (Math.random() - 0.5) * 1, 1656 vy: (Math.random() - 0.5) * 1, 1657 life: 1, 1658 maxLife: 2 + Math.random(), 1659 size: 12 + Math.random() * 10, 1660 rotation: Math.random() * Math.PI * 2, 1661 rotationSpeed: (Math.random() - 0.5) * 0.1, 1662 lightAngle: Math.random() * Math.PI * 2, 1663 type: 'prism' 1664 }); 1665 } 1666 1667 // ==================================== 1668 // NEW TRAILS â physics, ocean, nature, fantasy, food, casino, chat 1669 // ==================================== 1670 1671 function spawnChainLink(x, y) { 1672 if (Math.random() > 0.45) return; 1673 const parent = findLastParticleOfType('chainLink'); 1674 particles.push({ 1675 x: x + (Math.random() - 0.5) * 8, 1676 y: y + (Math.random() - 0.5) * 8, 1677 vx: (Math.random() - 0.5) * 2 + mouseVelX * 0.1, 1678 vy: (Math.random() - 0.5) * 2 + mouseVelY * 0.1, 1679 life: 1, 1680 maxLife: 3.5 + Math.random() * 2, 1681 size: 7 + Math.random() * 5, 1682 rotation: Math.random() * Math.PI * 2, 1683 rotationSpeed: (Math.random() - 0.5) * 0.15, 1684 bounce: 0.55, 1685 gravity: 0.28, 1686 parent: parent, 1687 linkLength: 14 + Math.random() * 6, 1688 color: `hsl(${35 + Math.random() * 25}, 50%, ${42 + Math.random() * 18}%)`, 1689 type: 'chainLink' 1690 }); 1691 } 1692 1693 function spawnDomino(x, y) { 1694 if (Math.random() > 0.5) return; 1695 particles.push({ 1696 x: x + (Math.random() - 0.5) * 12, 1697 y: y + (Math.random() - 0.5) * 12, 1698 vx: 0, 1699 vy: 0, 1700 life: 1, 1701 maxLife: 4 + Math.random() * 2, 1702 width: 6 + Math.random() * 4, 1703 height: 18 + Math.random() * 10, 1704 size: 12, 1705 rotation: 0, 1706 rotationSpeed: 0, 1707 fallen: false, 1708 bounce: 0.45, 1709 gravity: 0.32, 1710 dots: Math.floor(Math.random() * 6) + 1, 1711 color: '#f5f5f5', 1712 type: 'domino' 1713 }); 1714 } 1715 1716 function spawnSeahorse(x, y) { 1717 if (Math.random() > 0.55) return; 1718 particles.push({ 1719 x: x + (Math.random() - 0.5) * 14, 1720 y: y + (Math.random() - 0.5) * 14, 1721 vx: (Math.random() - 0.5) * 1.2, 1722 vy: -0.2 - Math.random() * 0.4, 1723 life: 1, 1724 maxLife: 3 + Math.random() * 2, 1725 size: 14 + Math.random() * 10, 1726 swimPhase: Math.random() * Math.PI * 2, 1727 direction: Math.random() > 0.5 ? 1 : -1, 1728 color: `hsl(${160 + Math.random() * 50}, ${55 + Math.random() * 25}%, ${45 + Math.random() * 20}%)`, 1729 type: 'seahorse' 1730 }); 1731 } 1732 1733 function spawnStarfish(x, y) { 1734 if (Math.random() > 0.5) return; 1735 particles.push({ 1736 x: x + (Math.random() - 0.5) * 16, 1737 y: y + (Math.random() - 0.5) * 16, 1738 vx: (Math.random() - 0.5) * 0.6, 1739 vy: 0.15 + Math.random() * 0.35, 1740 life: 1, 1741 maxLife: 3.5 + Math.random() * 2, 1742 size: 12 + Math.random() * 14, 1743 rotation: Math.random() * Math.PI * 2, 1744 rotationSpeed: (Math.random() - 0.5) * 0.06, 1745 arms: 5, 1746 color: `hsl(${10 + Math.random() * 30}, ${70 + Math.random() * 20}%, ${50 + Math.random() * 15}%)`, 1747 type: 'starfish' 1748 }); 1749 } 1750 1751 function spawnCoral(x, y) { 1752 if (Math.random() > 0.5) return; 1753 const branches = 3 + Math.floor(Math.random() * 4); 1754 const branchData = []; 1755 for (let i = 0; i < branches; i++) { 1756 branchData.push({ 1757 angle: (i / branches) * Math.PI - Math.PI / 2 + (Math.random() - 0.5) * 0.4, 1758 len: 0.6 + Math.random() * 0.5 1759 }); 1760 } 1761 particles.push({ 1762 x: x + (Math.random() - 0.5) * 14, 1763 y: y + (Math.random() - 0.5) * 14, 1764 vx: (Math.random() - 0.5) * 0.5, 1765 vy: -0.1 - Math.random() * 0.3, 1766 life: 1, 1767 maxLife: 3 + Math.random() * 2, 1768 size: 10 + Math.random() * 12, 1769 rotation: Math.random() * Math.PI * 2, 1770 rotationSpeed: (Math.random() - 0.5) * 0.08, 1771 branches: branches, 1772 branchData: branchData, 1773 color: `hsl(${330 + Math.random() * 40}, ${60 + Math.random() * 25}%, ${48 + Math.random() * 18}%)`, 1774 type: 'coral' 1775 }); 1776 } 1777 1778 function spawnMushroom(x, y) { 1779 if (Math.random() > 0.55) return; 1780 const capHue = Math.random() > 0.5 ? 0 + Math.random() * 20 : 270 + Math.random() * 40; 1781 particles.push({ 1782 x: x + (Math.random() - 0.5) * 10, 1783 y: y + (Math.random() - 0.5) * 10, 1784 vx: (Math.random() - 0.5) * 0.8, 1785 vy: 0.2 + Math.random() * 0.6, 1786 life: 1, 1787 maxLife: 2.5 + Math.random() * 1.5, 1788 size: 6 + Math.random() * 8, 1789 grow: 0.4, 1790 spots: Math.random() > 0.4, 1791 capColor: `hsl(${capHue}, ${65 + Math.random() * 25}%, ${48 + Math.random() * 15}%)`, 1792 stemColor: '#f5f0e6', 1793 type: 'mushroom' 1794 }); 1795 } 1796 1797 function spawnFeather(x, y) { 1798 if (Math.random() > 0.5) return; 1799 particles.push({ 1800 x: x + (Math.random() - 0.5) * 18, 1801 y: y + (Math.random() - 0.5) * 18, 1802 vx: (Math.random() - 0.5) * 1.5, 1803 vy: -0.3 - Math.random() * 0.8, 1804 life: 1, 1805 maxLife: 3 + Math.random() * 2, 1806 size: 14 + Math.random() * 12, 1807 rotation: Math.random() * Math.PI * 2, 1808 rotationSpeed: (Math.random() - 0.5) * 0.12, 1809 flutter: Math.random() * Math.PI * 2, 1810 color: `hsl(${0}, 0%, ${78 + Math.random() * 18}%)`, 1811 type: 'feather' 1812 }); 1813 } 1814 1815 function spawnFruit(x, y) { 1816 if (Math.random() > 0.5) return; 1817 const fruit = FRUIT_COLORS[Math.floor(Math.random() * FRUIT_COLORS.length)]; 1818 particles.push({ 1819 x: x + (Math.random() - 0.5) * 10, 1820 y: y + (Math.random() - 0.5) * 10, 1821 vx: (Math.random() - 0.5) * 2.5, 1822 vy: (Math.random() - 0.5) * 2, 1823 life: 1, 1824 maxLife: 3 + Math.random() * 2, 1825 size: 8 + Math.random() * 7, 1826 rotation: Math.random() * Math.PI * 2, 1827 rotationSpeed: (Math.random() - 0.5) * 0.15, 1828 bounce: 0.65, 1829 gravity: 0.3, 1830 fruitKind: fruit.kind, 1831 color: fruit.color, 1832 stemColor: fruit.stem, 1833 type: 'fruit' 1834 }); 1835 } 1836 1837 function spawnPortal(x, y) { 1838 if (Math.random() > 0.6) return; 1839 particles.push({ 1840 x: x + (Math.random() - 0.5) * 8, 1841 y: y + (Math.random() - 0.5) * 8, 1842 vx: (Math.random() - 0.5) * 0.3, 1843 vy: (Math.random() - 0.5) * 0.3, 1844 life: 1, 1845 maxLife: 1.8 + Math.random() * 1, 1846 size: 12 + Math.random() * 16, 1847 rotation: Math.random() * Math.PI * 2, 1848 rotationSpeed: 0.12 + Math.random() * 0.15, 1849 pulse: Math.random() * Math.PI * 2, 1850 innerHue: 260 + Math.random() * 60, 1851 type: 'portal' 1852 }); 1853 } 1854 1855 function spawnManaOrb(x, y) { 1856 if (Math.random() > 0.55) return; 1857 const orbitRadius = 20 + Math.random() * 25; 1858 const angle = Math.random() * Math.PI * 2; 1859 particles.push({
1860 x: x + Math.cos(angle) * orbitRadius, 1861 y: y + Math.sin(angle) * orbitRadius, 1862 centerX: x, 1863 centerY: y, 1864 orbitRadius: orbitRadius, 1865 orbitAngle: angle, 1866 orbitSpeed: 0.08 + Math.random() * 0.1, 1867 vx: 0, 1868 vy: 0, 1869 life: 1, 1870 maxLife: 2.5 + Math.random() * 1.5, 1871 size: 5 + Math.random() * 6, 1872 pulse: Math.random() * Math.PI * 2, 1873 color: `hsl(${200 + Math.random() * 80}, 85%, 65%)`, 1874 type: 'manaOrb' 1875 }); 1876 } 1877 1878 function spawnVinylRecord(x, y) { 1879 if (Math.random() > 0.5) return; 1880 particles.push({ 1881 x: x + (Math.random() - 0.5) * 12, 1882 y: y + (Math.random() - 0.5) * 12, 1883 vx: (Math.random() - 0.5) * 1.5, 1884 vy: 0.3 + Math.random() * 0.8, 1885 life: 1, 1886 maxLife: 3.5 + Math.random() * 2, 1887 size: 14 + Math.random() * 12, 1888 rotation: Math.random() * Math.PI * 2, 1889 rotationSpeed: 0.15 + Math.random() * 0.2, 1890 labelHue: Math.random() * 360, 1891 type: 'vinylRecord' 1892 }); 1893 } 1894 1895 function spawnCoffeeBean(x, y) { 1896 if (Math.random() > 0.45) return; 1897 particles.push({ 1898 x: x + (Math.random() - 0.5) * 14, 1899 y: y + (Math.random() - 0.5) * 14, 1900 vx: (Math.random() - 0.5) * 3, 1901 vy: (Math.random() - 0.5) * 2, 1902 life: 1, 1903 maxLife: 2.5 + Math.random() * 1.5, 1904 size: 5 + Math.random() * 5, 1905 rotation: Math.random() * Math.PI * 2, 1906 rotationSpeed: (Math.random() - 0.5) * 0.2, 1907 bounce: 0.5, 1908 gravity: 0.35, 1909 color: '#3e2723', 1910 type: 'coffeeBean' 1911 }); 1912 } 1913 1914 function spawnSprinkle(x, y) { 1915 if (Math.random() > 0.35) return; 1916 particles.push({ 1917 x: x + (Math.random() - 0.5) * 16, 1918 y: y + (Math.random() - 0.5) * 16, 1919 vx: (Math.random() - 0.5) * 2.5, 1920 vy: (Math.random() - 0.5) * 2, 1921 life: 1, 1922 maxLife: 2 + Math.random() * 1.5, 1923 size: 3 + Math.random() * 4, 1924 length: 6 + Math.random() * 8, 1925 rotation: Math.random() * Math.PI * 2, 1926 rotationSpeed: (Math.random() - 0.5) * 0.25, 1927 bounce: 0.55, 1928 gravity: 0.35, 1929 color: `hsl(${Math.random() * 360}, 90%, 60%)`, 1930 type: 'sprinkle' 1931 }); 1932 } 1933 1934 function spawnDice(x, y) { 1935 if (Math.random() > 0.5) return; 1936 const face = Math.floor(Math.random() * 6) + 1; 1937 particles.push({ 1938 x: x + (Math.random() - 0.5) * 12, 1939 y: y + (Math.random() - 0.5) * 12, 1940 vx: (Math.random() - 0.5) * 3, 1941 vy: (Math.random() - 0.5) * 2, 1942 life: 1, 1943 maxLife: 3 + Math.random() * 2, 1944 size: 10 + Math.random() * 8, 1945 rotation: Math.random() * Math.PI * 2, 1946 rotationSpeed: (Math.random() - 0.5) * 0.25, 1947 bounce: 0.6, 1948 gravity: 0.32, 1949 face: face, 1950 color: '#fffef8', 1951 pipColor: '#c62828', 1952 type: 'dice' 1953 }); 1954 } 1955 1956 function spawnChatBubble(x, y) { 1957 if (Math.random() > 0.45) return; 1958 const text = CHAT_BUBBLE_TEXTS[Math.floor(Math.random() * CHAT_BUBBLE_TEXTS.length)]; 1959 particles.push({ 1960 x: x + (Math.random() - 0.5) * 20, 1961 y: y + (Math.random() - 0.5) * 20, 1962 vx: (Math.random() - 0.5) * 0.8, 1963 vy: -0.6 - Math.random() * 0.8, 1964 life: 1, 1965 maxLife: 2.5 + Math.random() * 1.5, 1966 size: 10 + text.length * 2.5, 1967 text: text, 1968 wobble: Math.random() * Math.PI * 2, 1969 bubbleHue: Math.random() > 0.5 ? 280 + Math.random() * 40 : 190 + Math.random() * 40, 1970 type: 'chatBubble' 1971 }); 1972 } 1973 1974 function hexToRgba(hex, alpha) { 1975 const r = parseInt(hex.slice(1, 3), 16); 1976 const g = parseInt(hex.slice(3, 5), 16); 1977 const b = parseInt(hex.slice(5, 7), 16); 1978 return `rgba(${r}, ${g}, ${b}, ${alpha})`; 1979 } 1980 1981 function spawnSpaghetti(x, y) { 1982 const last = spaghettiPath[spaghettiPath.length - 1]; 1983 if (last) { 1984 const dx = x - last.x; 1985 const dy = y - last.y; 1986 if (dx * dx + dy * dy < 20) return; 1987 } 1988 1989 spaghettiPath.push({ 1990 x: x, 1991 y: y, 1992 life: 1, 1993 maxLife: 2.8 + Math.random() * 1.8, 1994 thickness: 3.5 + Math.random() * 3.5, 1995 wobble: Math.random() * Math.PI * 2, 1996 wobbleSpeed: 0.06 + Math.random() * 0.08, 1997 color: SPAGHETTI_COLORS[Math.floor(Math.random() * SPAGHETTI_COLORS.length)] 1998 }); 1999 2000 if (spaghettiPath.length > SPAGHETTI_MAX_POINTS) { 2001 spaghettiPath.shift(); 2002 } 2003 2004 if (Math.random() > 0.78) { 2005 particles.push({ 2006 x: x + (Math.random() - 0.5) * 8, 2007 y: y + (Math.random() - 0.5) * 8, 2008 vx: (Math.random() - 0.5) * 0.6, 2009 vy: 0.15 + Math.random() * 0.5, 2010 life: 1, 2011 maxLife: 1.5 + Math.random() * 1.2, 2012 size: 3 + Math.random() * 5, 2013 type: 'spaghettiSauce' 2014 }); 2015 } 2016 } 2017 2018 function updateAndDrawSpaghetti(deltaTime) { 2019 for (let i = spaghettiPath.length - 1; i >= 0; i--) { 2020 const pt = spaghettiPath[i]; 2021 pt.life -= (deltaTime * 0.016) / pt.maxLife; 2022 pt.wobble += pt.wobbleSpeed * deltaTime; 2023 if (pt.life <= 0) { 2024 spaghettiPath.splice(i, 1); 2025 } 2026 } 2027 2028 if (spaghettiPath.length < 2) return; 2029 2030 ctx.save(); 2031 ctx.lineCap = 'round'; 2032 ctx.lineJoin = 'round'; 2033 2034 for (let i = 1; i < spaghettiPath.length; i++) { 2035 const a = spaghettiPath[i - 1]; 2036 const b = spaghettiPath[i];
2037 const alpha = Math.min(a.life, b.life); 2038 if (alpha <= 0) continue; 2039 2040 const mx = (a.x + b.x) / 2; 2041 const my = (a.y + b.y) / 2; 2042 const dx = b.x - a.x; 2043 const dy = b.y - a.y; 2044 const len = Math.sqrt(dx * dx + dy * dy) || 1; 2045 const nx = -dy / len; 2046 const ny = dx / len; 2047 const wobbleOff = Math.sin(b.wobble) * (3 + b.thickness * 0.35); 2048 const thickness = ((a.thickness + b.thickness) / 2) * alpha; 2049 2050 ctx.strokeStyle = hexToRgba(a.color, alpha * 0.95); 2051 ctx.lineWidth = thickness; 2052 ctx.beginPath(); 2053 ctx.moveTo(a.x, a.y); 2054 ctx.quadraticCurveTo(mx + nx * wobbleOff, my + ny * wobbleOff, b.x, b.y); 2055 ctx.stroke(); 2056 2057 ctx.strokeStyle = hexToRgba('#fff8e7', alpha * 0.28); 2058 ctx.lineWidth = Math.max(1, thickness * 0.3); 2059 ctx.beginPath(); 2060 ctx.moveTo(a.x, a.y); 2061 ctx.quadraticCurveTo(mx + nx * wobbleOff * 0.6, my + ny * wobbleOff * 0.6, b.x, b.y); 2062 ctx.stroke(); 2063 } 2064 2065 ctx.restore(); 2066 } 2067 2068 // ==================================== 2069 // UPDATE & RENDER 2070 // ==================================== 2071 2072 let lastFrameTime = Date.now(); 2073 2074 function update() { 2075 if (!ctx || currentTrailType === 'none') { 2076 particles = []; 2077 spaghettiPath = []; 2078 return; 2079 } 2080 2081 // Calculate deltaTime 2082 const now = Date.now(); 2083 const deltaTime = Math.min((now - lastFrameTime) / 16.67, 2); // Cap at 2x to prevent huge jumps 2084 lastFrameTime = now; 2085 2086 // Clear canvas 2087 ctx.clearRect(0, 0, canvas.width, canvas.height); 2088 2089 if (currentTrailType === 'spaghetti') { 2090 updateAndDrawSpaghetti(deltaTime); 2091 } 2092 2093 if (window.PupwifeCursorTrailHandlers) { 2094 window.PupwifeCursorTrailHandlers.updateFrame(currentTrailType, { 2095 ctx: ctx, 2096 canvas: canvas, 2097 deltaTime: deltaTime, 2098 lastMouseX: lastMouseX, 2099 lastMouseY: lastMouseY 2100 }); 2101 } 2102 2103 if (isMatterTrail(currentTrailType) && window.PupwifeCursorTrailsMatter) { 2104 window.PupwifeCursorTrailsMatter.stepAndDraw(ctx, canvas, deltaTime); 2105 animationFrameId = requestAnimationFrame(update); 2106 return; 2107 } 2108 2109 // Update and draw particles 2110 for (let i = particles.length - 1; i >= 0; i--) { 2111 const p = particles[i]; 2112 2113 // Special update for angle-based particles (spiral, vortex, tornado) 2114 if (p.type === 'spiral' || p.type === 'vortex' || p.type === 'tornado') { 2115 p.angle += p.angleSpeed * deltaTime; 2116 p.radius += p.radiusSpeed * deltaTime; 2117 2118 // Update position based on angle and radius 2119 const centerX = p.centerX || 0; 2120 const centerY = p.centerY || 0; 2121 p.x = centerX + Math.cos(p.angle) * p.radius; 2122 p.y = centerY + Math.sin(p.angle) * p.radius; 2123 2124 // Also apply vertical movement for tornado 2125 if (p.type === 'tornado' && p.vy) { 2126 p.centerY += p.vy * deltaTime; 2127 } 2128 } else { 2129 // Normal position update 2130 p.x += p.vx * deltaTime; 2131 p.y += p.vy * deltaTime; 2132 } 2133 2134 // Update life 2135 p.life -= (deltaTime * 0.016) / p.maxLife; 2136 2137 // Apply gravity for some types 2138 if (p.type === 'bubbles' || p.type === 'snow' || p.type === 'leaves' || p.type === 'acorn' || p.type === 'pinecone' || 2139 p.type === 'starfish' || p.type === 'vinylRecord') { 2140 const gravity = p.gravity !== undefined ? p.gravity : 0.05; 2141 p.vy += gravity * deltaTime; 2142 } 2143 2144 // Physics bounce objects 2145 if (p.type === 'bouncyBall' || p.type === 'physicsBox' || p.type === 'chainLink' || 2146 p.type === 'fruit' || p.type === 'coffeeBean' || p.type === 'sprinkle' || p.type === 'dice' || 2147 (p.type === 'snowball' && !p.splatted) || 2148 (p.type === 'domino' && p.fallen)) { 2149 p.vy += p.gravity * deltaTime; 2150 2151 const halfW = p.type === 'domino' ? p.width : p.size; 2152 const halfH = p.type === 'domino' ? p.height : p.size; 2153 2154 if (p.x <= halfW || p.x >= canvas.width - halfW) { 2155 p.vx *= -p.bounce; 2156 p.x = Math.max(halfW, Math.min(canvas.width - halfW, p.x)); 2157 } 2158 if (p.y <= halfH || p.y >= canvas.height - halfH) { 2159 p.vy *= -p.bounce; 2160 p.y = Math.max(halfH, Math.min(canvas.height - halfH, p.y)); 2161 if (p.y >= canvas.height - halfH) { 2162 p.vx *= 0.92; 2163 if (p.type === 'domino' && p.fallen) { 2164 p.rotationSpeed *= 0.9; 2165 } 2166 } 2167 } 2168 } 2169 2170 // Chain link parent constraint 2171 if (p.type === 'chainLink' && p.parent && p.parent.life > 0) { 2172 const dx = p.x - p.parent.x; 2173 const dy = p.y - p.parent.y; 2174 const dist = Math.sqrt(dx * dx + dy * dy) || 0.001; 2175 if (dist > p.linkLength) { 2176 const pull = (dist - p.linkLength) * 0.08; 2177 p.vx -= (dx / dist) * pull; 2178 p.vy -= (dy / dist) * pull; 2179 p.parent.vx += (dx / dist) * pull * 0.5; 2180 p.parent.vy += (dy / dist) * pull * 0.5; 2181 } 2182 p.rotation = Math.atan2(dy, dx) + Math.PI / 2; 2183 } 2184 2185 // Domino tipping 2186 if (p.type === 'domino' && !p.fallen) { 2187 const speed = Math.sqrt(mouseVelX * mouseVelX + mouseVelY * mouseVelY); 2188 const dx = p.x - lastMouseX; 2189 const dy = p.y - lastMouseY; 2190 const distToCursor = Math.sqrt(dx * dx + dy * dy); 2191 if (speed > 8 && distToCursor < 40) { 2192 p.fallen = true; 2193 p.rotationSpeed = (mouseVelX > 0 ? 1 : -1) * (0.12 + speed * 0.008); 2194 p.vx = mouseVelX * 0.05; 2195 } else { 2196 for (let j = 0; j < particles.length; j++) { 2197 const other = particles[j]; 2198 if (other === p || other.type !== 'domino' || !other.fallen) continue; 2199 const odx = p.x - other.x; 2200 const ody = p.y - other.y; 2201 if (Math.sqrt(odx * odx + ody * ody) < 22) { 2202 p.fallen = true; 2203 p.rotationSpeed = other.rotationSpeed * 0.85; 2204 p.vx = other.vx * 0.5; 2205 break; 2206 } 2207 } 2208 } 2209 } 2210 2211 if (p.type === 'domino' && p.fallen) { 2212 p.rotation += p.rotationSpeed * deltaTime; 2213 p.vx *= 0.995; 2214 } 2215 2216 // Seahorse swim curve 2217 if (p.type === 'seahorse') { 2218 p.swimPhase += 0.12 * deltaTime; 2219 p.vx += Math.sin(p.swimPhase) * 0.08 * p.direction * deltaTime; 2220 p.vy += Math.cos(p.swimPhase * 0.7) * 0.04 * deltaTime; 2221 } 2222 2223 // Feather flutter 2224 if (p.type === 'feather') { 2225 p.flutter += 0.18 * deltaTime; 2226 p.vx += Math.sin(p.flutter) * 0.12 * deltaTime; 2227 p.vy += Math.cos(p.flutter * 0.6) * 0.05 * deltaTime; 2228 p.rotationSpeed += Math.sin(p.flutter) * 0.002; 2229 } 2230 2231 // Mushroom grow 2232 if (p.type === 'mushroom') { 2233 p.grow = Math.min(1, p.grow + 0.02 * deltaTime); 2234 p.vy += 0.04 * deltaTime; 2235 } 2236 2237 // Mana orb orbit 2238 if (p.type === 'manaOrb') { 2239 p.orbitAngle += p.orbitSpeed * deltaTime; 2240 p.centerX += mouseVelX * 0.02; 2241 p.centerY += mouseVelY * 0.02;
2242 p.x = p.centerX + Math.cos(p.orbitAngle) * p.orbitRadius * p.life; 2243 p.y = p.centerY + Math.sin(p.orbitAngle) * p.orbitRadius * p.life; 2244 p.pulse += 0.1 * deltaTime; 2245 } 2246 2247 // Portal pulse 2248 if (p.type === 'portal') { 2249 p.pulse += 0.14 * deltaTime; 2250 } 2251 2252 // Chat bubble float 2253 if (p.type === 'chatBubble') { 2254 p.wobble += 0.1 * deltaTime; 2255 p.vx += Math.sin(p.wobble) * 0.06 * deltaTime; 2256 } 2257 2258 // Spaghetti sauce droplets 2259 if (p.type === 'spaghettiSauce') { 2260 p.vy += 0.06 * deltaTime; 2261 } 2262 2263 if (window.PupwifeCursorTrailHandlers) { 2264 window.PupwifeCursorTrailHandlers.updateParticle(p, deltaTime, { 2265 canvas: canvas, 2266 mouseVelX: mouseVelX, 2267 mouseVelY: mouseVelY, 2268 lastMouseX: lastMouseX, 2269 lastMouseY: lastMouseY 2270 }); 2271 } 2272 2273 // Physics bounce â snowball 2274 if (p.type === 'snowball' && !p.splatted) { 2275 p.vx *= 0.99; 2276 } 2277 2278 // Coral drift 2279 if (p.type === 'coral') { 2280 p.vy += Math.sin(Date.now() * 0.002 + p.rotation) * 0.02 * deltaTime; 2281 } 2282 2283 // Handle raindrop splash 2284 if (p.type === 'raindrop' && p.splashY === null && p.y >= canvas.height - 20) { 2285 p.splashY = p.y; 2286 p.vx = 0; 2287 p.vy = 0; 2288 } 2289 2290 // Apply rotation 2291 if (p.rotation !== undefined) { 2292 p.rotation += p.rotationSpeed * deltaTime; 2293 } 2294 2295 // Remove dead particles 2296 if (p.life <= 0) { 2297 particles.splice(i, 1); 2298 continue; 2299 } 2300 2301 // Draw particle 2302 drawParticle(p); 2303 } 2304 2305 animationFrameId = requestAnimationFrame(update); 2306 } 2307 2308 const HANDLER_DRAW_TYPES = { 2309 tardigrade: true, 2310 seashell: true, 2311 seaweed: true, 2312 pearlBubble: true, 2313 bioluminescence: true, 2314 snowball: true, 2315 valentineCandy: true, 2316 tetromino: true, 2317 pokerChip: true, 2318 xpOrb: true, 2319 pixelHeart: true, 2320 paperClip: true, 2321 stickyNote: true, 2322 popcorn: true, 2323 gummyWoim: true, 2324 shootingStar: true, 2325 snail: true, 2326 moth: true, 2327 bat: true, 2328 frog: true, 2329 woim: true, 2330 equalizerBar: true, 2331 glitterSparkle: true, 2332 moonDust: true 2333 }; 2334 2335 function drawParticle(p) { 2336 ctx.save(); 2337 ctx.globalAlpha = p.life; 2338 2339 ctx.translate(p.x, p.y); 2340 if (p.rotation !== undefined) { 2341 ctx.rotate(p.rotation); 2342 } 2343 2344 switch (p.type) { 2345 case 'sparkle': drawSparkle(p); break; 2346 case 'fairy': drawFairy(p); break; 2347 case 'rainbow': 2348 case 'fire': drawCircle(p); break; 2349 case 'bubble': drawBubble(p); break; 2350 case 'heart': drawHeart(p); break; 2351 case 'star': drawStar(p); break; 2352 case 'snow': drawSnowflake(p); break; 2353 case 'leaf': drawLeaf(p); break; 2354 // NEW DRAW CASES 2355 case 'lightning': drawLightning(p); break; 2356 case 'cherryBlossom': drawCherryBlossom(p); break; 2357 case 'firefly': drawFirefly(p); break; 2358 case 'butterfly': drawButterfly(p); break; 2359 case 'comet': drawComet(p); break; 2360 case 'magicRune': drawMagicRune(p); break; 2361 case 'crystal': drawCrystal(p); break; 2362 case 'energyOrb': drawEnergyOrb(p); break; 2363 case 'matrix': drawMatrix(p); break; 2364 case 'neonLine': drawNeonLine(p); break; 2365 case 'glitch': drawGlitch(p); break; 2366 case 'musicNote': drawMusicNote(p); break; 2367 case 'confetti': drawConfetti(p); break; 2368 case 'paintSplatter': drawPaintSplatter(p); break; 2369 case 'smoke': drawSmoke(p); break; 2370 case 'ripple': drawRipple(p); break; 2371 case 'dna': drawDNA(p); break; 2372 case 'circuit': drawCircuit(p); break; 2373 case 'binary': drawBinary(p); break; 2374 case 'pawPrint': drawPawPrint(p); break; 2375 case 'coloredHeart': drawColoredHeart(p); break; 2376 case 'dandelion': drawDandelion(p); break; 2377 case 'rosePetal': drawRosePetal(p); break; 2378 case 'pixelated': drawPixel(p); break; 2379 case 'galaxy': drawGalaxy(p); break; 2380 case 'auroraWave': drawAuroraWave(p); break; 2381 case 'geometric': drawGeometric(p); break; 2382 case 'spiral': drawSpiral(p); break; 2383 case 'inkBlot': drawInkBlot(p); break; 2384 case 'paintStroke': drawPaintStroke(p); break; 2385 case 'laser': drawLaser(p); break; 2386 case 'hologram': drawHologram(p); break; 2387 case 'cloud': drawCloud(p); break; 2388 case 'tornado': drawTornado(p); break; 2389 case 'emoji': drawEmoji(p); break; 2390 case 'candy': drawCandy(p); break; 2391 case 'electricSpark': drawElectricSpark(p); break; 2392 case 'plasma': drawPlasma(p); break;
2393 case 'vortex': drawVortex(p); break; 2394 case 'cosmicDust': drawCosmicDust(p); break; 2395 // NEW PHYSICS & OCEAN TRAILS 2396 case 'bouncyBall': drawBouncyBall(p); break; 2397 case 'physicsBox': drawPhysicsBox(p); break; 2398 case 'jellyfish': drawJellyfish(p); break; 2399 case 'fish': drawFish(p); break; 2400 case 'acorn': drawAcorn(p); break; 2401 case 'pinecone': drawPinecone(p); break; 2402 case 'raindrop': drawRaindrop(p); break; 2403 case 'mist': drawMist(p); break; 2404 case 'fog': drawFog(p); break; 2405 case 'chalk': drawChalk(p); break; 2406 case 'pastel': drawPastel(p); break; 2407 case 'dataPacket': drawDataPacket(p); break; 2408 case 'soundWave': drawSoundWave(p); break; 2409 case 'trafficLight': drawTrafficLight(p); break; 2410 case 'bee': drawBee(p); break; 2411 case 'lensFlare': drawLensFlare(p); break; 2412 case 'prism': drawPrism(p); break; 2413 case 'chainLink': drawChainLink(p); break; 2414 case 'domino': drawDomino(p); break; 2415 case 'seahorse': drawSeahorse(p); break; 2416 case 'starfish': drawStarfish(p); break; 2417 case 'coral': drawCoral(p); break; 2418 case 'mushroom': drawMushroom(p); break; 2419 case 'feather': drawFeather(p); break; 2420 case 'fruit': drawFruit(p); break; 2421 case 'portal': drawPortal(p); break; 2422 case 'manaOrb': drawManaOrb(p); break; 2423 case 'vinylRecord': drawVinylRecord(p); break; 2424 case 'coffeeBean': drawCoffeeBean(p); break; 2425 case 'sprinkle': drawSprinkle(p); break; 2426 case 'dice': drawDice(p); break; 2427 case 'chatBubble': drawChatBubble(p); break; 2428 case 'spaghettiSauce': drawSpaghettiSauce(p); break; 2429 } 2430 2431 if (window.PupwifeCursorTrailHandlers && 2432 window.PupwifeCursorTrailHandlers.draw && 2433 HANDLER_DRAW_TYPES[p.type]) { 2434 window.PupwifeCursorTrailHandlers.draw(ctx, p); 2435 } 2436 2437 ctx.restore(); 2438 } 2439 2440 function drawSparkle(p) { 2441 ctx.fillStyle = p.color; 2442 ctx.fillRect(-p.size/2, -1, p.size, 2); 2443 ctx.fillRect(-1, -p.size/2, 2, p.size); 2444 } 2445 2446 function drawFairy(p) { 2447 ctx.shadowBlur = p.glow; 2448 ctx.shadowColor = p.color; 2449 ctx.fillStyle = p.color; 2450 ctx.beginPath(); 2451 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 2452 ctx.fill(); 2453 } 2454 2455 function drawCircle(p) { 2456 ctx.fillStyle = p.color; 2457 ctx.beginPath(); 2458 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 2459 ctx.fill(); 2460 } 2461 2462 function drawBubble(p) { 2463 // Outer bubble 2464 ctx.strokeStyle = `rgba(200, 230, 255, ${p.life * 0.6})`; 2465 ctx.lineWidth = 2; 2466 ctx.beginPath(); 2467 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 2468 ctx.stroke(); 2469 2470 // Inner highlight 2471 ctx.fillStyle = `rgba(255, 255, 255, ${p.life * 0.3})`; 2472 ctx.beginPath(); 2473 ctx.arc(-p.size * 0.3, -p.size * 0.3, p.size * 0.3, 0, Math.PI * 2); 2474 ctx.fill(); 2475 } 2476 2477 function drawHeart(p) { 2478 ctx.fillStyle = `rgba(255, 105, 180, ${p.life})`; 2479 2480 const size = p.size; 2481 ctx.beginPath(); 2482 ctx.moveTo(0, size * 0.3); 2483 ctx.bezierCurveTo(-size * 0.5, -size * 0.3, -size, size * 0.1, 0, size * 0.8); 2484 ctx.bezierCurveTo(size, size * 0.1, size * 0.5, -size * 0.3, 0, size * 0.3); 2485 ctx.fill(); 2486 } 2487 2488 function drawStar(p) { 2489 ctx.fillStyle = `rgba(255, 223, 0, ${p.life})`; 2490 ctx.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.8})`; 2491 ctx.lineWidth = 2; 2492 2493 const size = p.size; 2494 const points = p.points; 2495 2496 ctx.beginPath(); 2497 for (let i = 0; i < points * 2; i++) { 2498 const angle = (i * Math.PI) / points; 2499 const radius = i % 2 === 0 ? size : size * 0.5; 2500 const x = Math.cos(angle) * radius; 2501 const y = Math.sin(angle) * radius; 2502 2503 if (i === 0) ctx.moveTo(x, y); 2504 else ctx.lineTo(x, y); 2505 } 2506 ctx.closePath(); 2507 ctx.fill(); 2508 ctx.stroke(); 2509 } 2510 2511 function drawSnowflake(p) { 2512 ctx.strokeStyle = `rgba(240, 248, 255, ${p.life})`; 2513 ctx.fillStyle = `rgba(255, 255, 255, ${p.life})`; 2514 ctx.lineWidth = 2; 2515 2516 const size = p.size; 2517 2518 // Simple snowflake 2519 for (let i = 0; i < 6; i++) { 2520 ctx.save(); 2521 ctx.rotate((i * Math.PI) / 3); 2522 ctx.beginPath(); 2523 ctx.moveTo(0, 0); 2524 ctx.lineTo(0, -size); 2525 ctx.stroke();
2526 ctx.restore(); 2527 } 2528 2529 // Center dot 2530 ctx.beginPath(); 2531 ctx.arc(0, 0, size * 0.15, 0, Math.PI * 2); 2532 ctx.fill(); 2533 } 2534 2535 function drawLeaf(p) { 2536 ctx.fillStyle = p.color; 2537 2538 const size = p.size; 2539 2540 // Leaf shape 2541 ctx.beginPath(); 2542 ctx.moveTo(0, -size * 0.5); 2543 ctx.quadraticCurveTo(size * 0.5, -size * 0.2, size * 0.3, size * 0.5); 2544 ctx.quadraticCurveTo(0, size * 0.3, 0, -size * 0.5); 2545 ctx.quadraticCurveTo(-size * 0.5, -size * 0.2, -size * 0.3, size * 0.5); 2546 ctx.quadraticCurveTo(0, size * 0.3, 0, -size * 0.5); 2547 ctx.fill(); 2548 2549 // Vein 2550 ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)'; 2551 ctx.lineWidth = 1; 2552 ctx.beginPath(); 2553 ctx.moveTo(0, -size * 0.5); 2554 ctx.lineTo(0, size * 0.5); 2555 ctx.stroke(); 2556 } 2557 2558 // ==================================== 2559 // NEW DRAW FUNCTIONS (40 MORE!) 2560 // ==================================== 2561 2562 function drawLightning(p) { 2563 ctx.strokeStyle = p.color; 2564 ctx.lineWidth = 3; 2565 ctx.shadowBlur = 15; 2566 ctx.shadowColor = p.color; 2567 2568 ctx.beginPath(); 2569 ctx.moveTo(0, 0); 2570 for (let seg of p.segments) { 2571 ctx.lineTo(seg.x, seg.y); 2572 } 2573 ctx.stroke(); 2574 } 2575 2576 function drawCherryBlossom(p) { 2577 p.wobble += 0.05; 2578 const wobbleX = Math.sin(p.wobble) * 3; 2579 2580 ctx.fillStyle = '#ffc0cb'; 2581 for (let i = 0; i < 5; i++) { 2582 ctx.save(); 2583 ctx.rotate((i * Math.PI * 2) / 5); 2584 ctx.translate(wobbleX, 0); 2585 ctx.beginPath(); 2586 ctx.ellipse(p.size * 0.6, 0, p.size * 0.5, p.size * 0.3, 0, 0, Math.PI * 2); 2587 ctx.fill(); 2588 ctx.restore(); 2589 } 2590 2591 // Center 2592 ctx.fillStyle = '#ffeb3b'; 2593 ctx.beginPath(); 2594 ctx.arc(0, 0, p.size * 0.2, 0, Math.PI * 2); 2595 ctx.fill(); 2596 } 2597 2598 function drawFirefly(p) { 2599 p.pulse += 0.15; 2600 const brightness = 0.5 + Math.sin(p.pulse) * 0.5; 2601 2602 ctx.shadowBlur = p.glow * brightness; 2603 ctx.shadowColor = p.color; 2604 ctx.fillStyle = p.color; 2605 ctx.globalAlpha = brightness; 2606 ctx.beginPath(); 2607 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 2608 ctx.fill(); 2609 ctx.globalAlpha = 1; 2610 } 2611 2612 function drawButterfly(p) { 2613 p.wingFlap += 0.3; 2614 const flapAngle = Math.sin(p.wingFlap) * 0.3; 2615 2616 // Left wing 2617 ctx.save(); 2618 ctx.rotate(flapAngle); 2619 ctx.fillStyle = p.color; 2620 ctx.beginPath(); 2621 ctx.ellipse(-p.size * 0.5, 0, p.size * 0.6, p.size * 0.4, 0, 0, Math.PI * 2); 2622 ctx.fill(); 2623 ctx.restore(); 2624 2625 // Right wing 2626 ctx.save(); 2627 ctx.rotate(-flapAngle); 2628 ctx.fillStyle = p.color; 2629 ctx.beginPath(); 2630 ctx.ellipse(p.size * 0.5, 0, p.size * 0.6, p.size * 0.4, 0, 0, Math.PI * 2); 2631 ctx.fill(); 2632 ctx.restore(); 2633 2634 // Body 2635 ctx.fillStyle = '#000'; 2636 ctx.fillRect(-p.size * 0.1, -p.size * 0.4, p.size * 0.2, p.size * 0.8); 2637 } 2638 2639 function drawComet(p) { 2640 // Track tail 2641 p.tail.push({x: 0, y: 0}); 2642 if (p.tail.length > 8) p.tail.shift(); 2643 2644 // Draw tail 2645 ctx.strokeStyle = p.color; 2646 ctx.lineWidth = 3; 2647 ctx.beginPath(); 2648 for (let i = 0; i < p.tail.length; i++) { 2649 ctx.globalAlpha = (i / p.tail.length) * p.life; 2650 const point = p.tail[i]; 2651 if (i === 0) ctx.moveTo(point.x, point.y); 2652 else ctx.lineTo(point.x, point.y); 2653 } 2654 ctx.stroke(); 2655 ctx.globalAlpha = 1; 2656 2657 // Draw head 2658 ctx.fillStyle = p.color; 2659 ctx.shadowBlur = 10; 2660 ctx.shadowColor = p.color; 2661 ctx.beginPath(); 2662 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 2663 ctx.fill(); 2664 } 2665 2666 function drawMagicRune(p) { 2667 ctx.fillStyle = p.color; 2668 ctx.shadowBlur = 15; 2669 ctx.shadowColor = p.color; 2670 ctx.font = `${p.size}px serif`; 2671 ctx.textAlign = 'center'; 2672 ctx.textBaseline = 'middle'; 2673 ctx.fillText(p.rune, 0, 0); 2674 } 2675 2676 function drawCrystal(p) { 2677 ctx.fillStyle = p.color; 2678 ctx.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.8})`; 2679 ctx.lineWidth = 2; 2680 2681 ctx.beginPath(); 2682 for (let i = 0; i < p.sides; i++) { 2683 const angle = (i * Math.PI * 2) / p.sides; 2684 const x = Math.cos(angle) * p.size; 2685 const y = Math.sin(angle) * p.size; 2686 if (i === 0) ctx.moveTo(x, y); 2687 else ctx.lineTo(x, y); 2688 } 2689 ctx.closePath(); 2690 ctx.fill(); 2691 ctx.stroke(); 2692 } 2693 2694 function drawEnergyOrb(p) { 2695 p.pulse += 0.2;
2696 const pulseSize = p.size + Math.sin(p.pulse) * 3; 2697 2698 ctx.shadowBlur = p.glow; 2699 ctx.shadowColor = p.color; 2700 ctx.fillStyle = p.color; 2701 ctx.beginPath(); 2702 ctx.arc(0, 0, pulseSize, 0, Math.PI * 2); 2703 ctx.fill(); 2704 } 2705 2706 function drawMatrix(p) { 2707 ctx.fillStyle = `rgba(0, 255, 0, ${p.life})`; 2708 ctx.font = `${p.size}px monospace`; 2709 ctx.textAlign = 'center'; 2710 ctx.textBaseline = 'middle'; 2711 ctx.fillText(p.char, 0, 0); 2712 } 2713 2714 function drawNeonLine(p) { 2715 ctx.save(); 2716 ctx.rotate(p.angle); 2717 ctx.strokeStyle = p.color; 2718 ctx.lineWidth = 3; 2719 ctx.shadowBlur = p.glow; 2720 ctx.shadowColor = p.color; 2721 ctx.beginPath(); 2722 ctx.moveTo(0, 0); 2723 ctx.lineTo(p.length, 0); 2724 ctx.stroke(); 2725 ctx.restore(); 2726 } 2727 2728 function drawGlitch(p) { 2729 ctx.fillStyle = p.color; 2730 ctx.fillRect(-p.width/2, -p.height/2, p.width, p.height); 2731 } 2732 2733 function drawMusicNote(p) { 2734 ctx.fillStyle = p.color; 2735 ctx.font = `${p.size}px Arial`; 2736 ctx.textAlign = 'center'; 2737 ctx.textBaseline = 'middle'; 2738 ctx.fillText(p.note, 0, 0); 2739 } 2740 2741 function drawConfetti(p) { 2742 ctx.fillStyle = p.color; 2743 if (p.shape === 'circle') { 2744 ctx.beginPath(); 2745 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 2746 ctx.fill(); 2747 } else { 2748 ctx.fillRect(-p.size/2, -p.size/2, p.size, p.size); 2749 } 2750 } 2751 2752 function drawPaintSplatter(p) { 2753 ctx.fillStyle = p.color; 2754 2755 // Main splat 2756 ctx.beginPath(); 2757 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 2758 ctx.fill(); 2759 2760 // Splatter drips 2761 for (let splat of p.splats) { 2762 ctx.beginPath(); 2763 ctx.arc( 2764 Math.cos(splat.angle) * splat.length, 2765 Math.sin(splat.angle) * splat.length, 2766 p.size * 0.3, 2767 0, 2768 Math.PI * 2 2769 ); 2770 ctx.fill(); 2771 } 2772 } 2773 2774 function drawSmoke(p) { 2775 p.expansion += 0.02; 2776 const expandedSize = p.size * p.expansion; 2777 2778 ctx.fillStyle = `rgba(100, 100, 100, ${p.life * 0.3})`; 2779 ctx.beginPath(); 2780 ctx.arc(0, 0, expandedSize, 0, Math.PI * 2); 2781 ctx.fill(); 2782 } 2783 2784 function drawRipple(p) { 2785 const progress = 1 - p.life; 2786 const currentSize = p.size + (p.maxSize - p.size) * progress; 2787 2788 ctx.strokeStyle = `rgba(100, 200, 255, ${p.life})`; 2789 ctx.lineWidth = 2; 2790 ctx.beginPath(); 2791 ctx.arc(0, 0, currentSize, 0, Math.PI * 2); 2792 ctx.stroke(); 2793 } 2794 2795 function drawDNA(p) { 2796 p.twist += 0.1; 2797 2798 const height = p.size * 4; 2799 ctx.strokeStyle = '#0ff'; 2800 ctx.lineWidth = 2; 2801 2802 // Draw double helix 2803 ctx.beginPath(); 2804 for (let i = 0; i < 20; i++) { 2805 const y = (i / 20 - 0.5) * height; 2806 const x = Math.sin(p.twist + i * 0.3) * p.size; 2807 if (i === 0) ctx.moveTo(x, y); 2808 else ctx.lineTo(x, y); 2809 } 2810 ctx.stroke(); 2811 2812 ctx.strokeStyle = '#f0f'; 2813 ctx.beginPath(); 2814 for (let i = 0; i < 20; i++) { 2815 const y = (i / 20 - 0.5) * height; 2816 const x = -Math.sin(p.twist + i * 0.3) * p.size; 2817 if (i === 0) ctx.moveTo(x, y); 2818 else ctx.lineTo(x, y); 2819 } 2820 ctx.stroke(); 2821 } 2822 2823 function drawCircuit(p) { 2824 ctx.strokeStyle = '#0f0'; 2825 ctx.fillStyle = '#0f0'; 2826 ctx.lineWidth = 2; 2827 2828 // Draw node 2829 ctx.beginPath(); 2830 ctx.arc(0, 0, p.size * 0.3, 0, Math.PI * 2); 2831 ctx.fill(); 2832 2833 // Draw lines 2834 for (let line of p.lines) { 2835 ctx.beginPath(); 2836 ctx.moveTo(0, 0); 2837 ctx.lineTo( 2838 Math.cos(line.angle) * line.length, 2839 Math.sin(line.angle) * line.length 2840 ); 2841 ctx.stroke(); 2842 } 2843 } 2844 2845 function drawBinary(p) { 2846 ctx.fillStyle = `rgba(0, 255, 0, ${p.life})`; 2847 ctx.font = `${p.size}px monospace`; 2848 ctx.textAlign = 'center'; 2849 ctx.textBaseline = 'middle'; 2850 ctx.fillText(p.digit, 0, 0); 2851 } 2852 2853 function drawPawPrint(p) { 2854 if (pawPrintImage && pawPrintImageLoaded && pawPrintImage.complete && pawPrintImage.naturalWi
2854dth > 0) { 2855 // Image is loaded, use it to create a colored paw print 2856 ctx.save(); 2857 ctx.globalAlpha = p.life * 0.9; 2858 2859 // Draw the image first to establish the alpha mask 2860 ctx.drawImage(pawPrintImage, -p.size/2, -p.size/2, p.size, p.size); 2861 2862 // Use source-atop to fill only the drawn area with our color 2863 // This replaces the black paw print with our color while keeping the shape 2864 ctx.globalCompositeOperation = 'source-atop'; 2865 ctx.fillStyle = p.color || '#ff69b4'; 2866 ctx.fillRect(-p.size/2, -p.size/2, p.size, p.size); 2867 2868 ctx.restore(); 2869 } else { 2870 // Fallback: draw simple paw print if image hasn't loaded yet 2871 ctx.fillStyle = p.color || '#ff69b4'; 2872 ctx.globalAlpha = p.life; 2873 2874 // Main pad 2875 ctx.beginPath(); 2876 ctx.ellipse(0, p.size * 0.2, p.size * 0.6, p.size * 0.5, 0, 0, Math.PI * 2); 2877 ctx.fill(); 2878 2879 // Toes 2880 for (let i = 0; i < 4; i++) { 2881 const angle = (i - 1.5) * 0.5; 2882 const x = Math.sin(angle) * p.size * 0.5; 2883 const y = -p.size * 0.4; 2884 ctx.beginPath(); 2885 ctx.ellipse(x, y, p.size * 0.25, p.size * 0.35, 0, 0, Math.PI * 2); 2886 ctx.fill(); 2887 } 2888 2889 ctx.globalAlpha = 1; 2890 } 2891 } 2892 2893 function drawColoredHeart(p) { 2894 ctx.font = `${p.size}px Arial`; 2895 ctx.textAlign = 'center'; 2896 ctx.textBaseline = 'middle'; 2897 ctx.fillText(p.emoji, 0, 0); 2898 } 2899 2900 function drawDandelion(p) { 2901 ctx.strokeStyle = `rgba(255, 255, 255, ${p.life})`; 2902 ctx.fillStyle = `rgba(255, 255, 255, ${p.life})`; 2903 ctx.lineWidth = 1; 2904 2905 // Seeds radiating out 2906 for (let i = 0; i < 8; i++) { 2907 const angle = (i * Math.PI * 2) / 8; 2908 ctx.beginPath(); 2909 ctx.moveTo(0, 0); 2910 ctx.lineTo(Math.cos(angle) * p.size, Math.sin(angle) * p.size); 2911 ctx.stroke(); 2912 2913 ctx.beginPath(); 2914 ctx.arc(Math.cos(angle) * p.size, Math.sin(angle) * p.size, 1, 0, Math.PI * 2); 2915 ctx.fill(); 2916 } 2917 } 2918 2919 function drawRosePetal(p) { 2920 p.wobble += 0.05; 2921 const wobbleX = Math.sin(p.wobble) * 2; 2922 2923 ctx.fillStyle = p.color; 2924 ctx.translate(wobbleX, 0); 2925 ctx.beginPath(); 2926 ctx.ellipse(0, 0, p.size * 0.7, p.size, 0, 0, Math.PI * 2); 2927 ctx.fill(); 2928 } 2929 2930 function drawPixel(p) { 2931 ctx.fillStyle = p.color; 2932 ctx.fillRect(-p.size/2, -p.size/2, p.size, p.size); 2933 } 2934 2935 function drawGalaxy(p) { 2936 p.orbitAngle += p.orbitSpeed; 2937 const orbitX = Math.cos(p.orbitAngle) * 3; 2938 const orbitY = Math.sin(p.orbitAngle) * 3; 2939 2940 ctx.shadowBlur = 10; 2941 ctx.shadowColor = p.color; 2942 ctx.fillStyle = p.color; 2943 ctx.beginPath(); 2944 ctx.arc(orbitX, orbitY, p.size, 0, Math.PI * 2); 2945 ctx.fill(); 2946 } 2947 2948 function drawAuroraWave(p) { 2949 p.wave += 0.1; 2950 2951 ctx.fillStyle = p.color; 2952 ctx.globalAlpha = p.life * 0.4; 2953 ctx.beginPath(); 2954 for (let i = 0; i < p.width; i += 5) { 2955 const x = i - p.width / 2; 2956 const y = Math.sin(p.wave + i * 0.1) * p.height * 0.5; 2957 if (i === 0) ctx.moveTo(x, y - p.height); 2958 else ctx.lineTo(x, y); 2959 } 2960 ctx.lineTo(p.width / 2, p.height); 2961 ctx.lineTo(-p.width / 2, p.height); 2962 ctx.closePath(); 2963 ctx.fill(); 2964 ctx.globalAlpha = 1; 2965 } 2966 2967 function drawGeometric(p) { 2968 // Draw wireframe geometric shapes with inner patterns 2969 ctx.strokeStyle = p.color; 2970 ctx.lineWidth = 3; 2971 ctx.shadowBlur = 8; 2972 ctx.shadowColor = p.color; 2973 2974 // Outer shape 2975 ctx.beginPath(); 2976 for (let i = 0; i < p.sides; i++) { 2977 const angle = (i * Math.PI * 2) / p.sides; 2978 const x = Math.cos(angle) * p.size; 2979 const y = Math.sin(angle) * p.size; 2980 if (i === 0) ctx.moveTo(x, y); 2981 else ctx.lineTo(x, y); 2982 } 2983 ctx.closePath(); 2984 ctx.stroke(); 2985 2986 // Inner pattern (connecting to center) 2987 ctx.lineWidth = 1; 2988 ctx.globalAlpha = p.life * 0.5; 2989 for (let i = 0; i < p.sides; i++) { 2990 const angle = (i * Math.PI * 2) / p.sides; 2991 const x = Math.cos(angle) * p.size; 2992 const y = Math.sin(angle) * p.size; 2993 ctx.beginPath(); 2994 ctx.moveTo(0, 0); 2995 ctx.lineTo(x, y); 2996 ctx.stroke(); 2997 } 2998 ctx.globalAlpha = 1; 2999 } 3000 3001 function drawSpiral(p) { 3002 ctx.strokeStyle = p.color; 3003 ctx.lineWidth = 3; 3004 ctx.shadowBlur = 10; 3005 ctx.shadowColor = p.color; 3006 3007 ctx.beginPath(); 3008 ctx.arc(0, 0, 5, 0, Math.PI * 2); 3009 ctx.fill(); 3010 } 3011 3012 function drawInkBlot(p) { 3013 p.expansion += 0.02; 3014 3015 ctx.fillStyle = p.color; 3016 ctx.globalAlpha = p.life * 0.7; 3017 3018 // Main blob 3019 ctx.beginPath(); 3020 ctx.arc(0, 0, p.size * p.expansion, 0, Math.PI * 2); 3021 ctx.fill(); 3022 3023 // Extra blobs 3024 for (let blob of p.blobs) { 3025 ctx.beginPath(); 3026 ctx.arc(blob.x, blob.y, blob.size * p.expansion, 0, Math.PI * 2); 3027 ctx.fill(); 3028 } 3029 3030 ctx.globalAlpha = 1; 3031 } 3032 3033 function drawPaintStroke(p) { 3034 ctx.save(); 3035 ctx.rotate(p.angle); 3036 ctx.fillStyle = p.color; 3037 ctx.fillRect(0, -p.width/2, p.length, p.width);
3038 ctx.restore(); 3039 } 3040 3041 function drawLaser(p) { 3042 ctx.save(); 3043 ctx.rotate(p.angle); 3044 ctx.strokeStyle = p.color; 3045 ctx.lineWidth = 4; 3046 ctx.shadowBlur = p.glow; 3047 ctx.shadowColor = p.color; 3048 ctx.beginPath(); 3049 ctx.moveTo(0, 0); 3050 ctx.lineTo(p.length, 0); 3051 ctx.stroke(); 3052 ctx.restore(); 3053 } 3054 3055 function drawHologram(p) { 3056 ctx.strokeStyle = p.color; 3057 ctx.fillStyle = p.color; 3058 ctx.globalAlpha = p.life * 0.6; 3059 3060 // Main shape 3061 ctx.beginPath(); 3062 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 3063 ctx.stroke(); 3064 3065 // Scan lines 3066 for (let i = 0; i < p.scanLines; i++) { 3067 const y = -p.size + (i * p.size * 2) / p.scanLines; 3068 ctx.beginPath(); 3069 ctx.moveTo(-p.size, y); 3070 ctx.lineTo(p.size, y); 3071 ctx.stroke(); 3072 } 3073 3074 ctx.globalAlpha = 1; 3075 } 3076 3077 function drawCloud(p) { 3078 ctx.fillStyle = `rgba(255, 255, 255, ${p.life * 0.7})`; 3079 3080 for (let puff of p.puffs) { 3081 ctx.beginPath(); 3082 ctx.arc(puff.x, puff.y, puff.size, 0, Math.PI * 2); 3083 ctx.fill(); 3084 } 3085 } 3086 3087 function drawTornado(p) { 3088 ctx.fillStyle = p.color; 3089 ctx.beginPath(); 3090 ctx.arc(0, 0, 3, 0, Math.PI * 2); 3091 ctx.fill(); 3092 } 3093 3094 function drawEmoji(p) { 3095 ctx.font = `${p.size}px Arial`; 3096 ctx.textAlign = 'center'; 3097 ctx.textBaseline = 'middle'; 3098 ctx.fillText(p.emoji, 0, 0); 3099 } 3100 3101 function drawCandy(p) { 3102 ctx.fillStyle = p.color; 3103 3104 if (p.candyType === 0) { 3105 // Lollipop 3106 ctx.beginPath(); 3107 ctx.arc(0, -p.size * 0.3, p.size * 0.6, 0, Math.PI * 2); 3108 ctx.fill(); 3109 ctx.strokeStyle = p.color; 3110 ctx.lineWidth = 3; 3111 ctx.beginPath(); 3112 ctx.moveTo(0, p.size * 0.3); 3113 ctx.lineTo(0, p.size); 3114 ctx.stroke(); 3115 } else if (p.candyType === 1) { 3116 // Wrapped candy 3117 ctx.fillRect(-p.size * 0.6, -p.size * 0.3, p.size * 1.2, p.size * 0.6); 3118 ctx.strokeStyle = '#fff'; 3119 ctx.lineWidth = 2; 3120 ctx.beginPath(); 3121 ctx.moveTo(-p.size * 0.6, 0); 3122 ctx.lineTo(-p.size * 0.8, -p.size * 0.5); 3123 ctx.moveTo(p.size * 0.6, 0); 3124 ctx.lineTo(p.size * 0.8, -p.size * 0.5); 3125 ctx.stroke(); 3126 } else { 3127 // Heart candy 3128 ctx.beginPath(); 3129 ctx.moveTo(0, p.size * 0.2); 3130 ctx.bezierCurveTo(-p.size * 0.4, -p.size * 0.2, -p.size * 0.8, p.size * 0.1, 0, p.size * 0.7); 3131 ctx.bezierCurveTo(p.size * 0.8, p.size * 0.1, p.size * 0.4, -p.size * 0.2, 0, p.size * 0.2); 3132 ctx.fill(); 3133 } 3134 } 3135 3136 function drawElectricSpark(p) { 3137 ctx.strokeStyle = p.color; 3138 ctx.lineWidth = 2; 3139 ctx.shadowBlur = p.glow; 3140 ctx.shadowColor = p.color; 3141 3142 // Center 3143 ctx.fillStyle = p.color; 3144 ctx.beginPath(); 3145 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 3146 ctx.fill(); 3147 3148 // Branches 3149 for (let branch of p.branches) { 3150 ctx.beginPath(); 3151 ctx.moveTo(0, 0); 3152 ctx.lineTo( 3153 Math.cos(branch.angle) * branch.length, 3154 Math.sin(branch.angle) * branch.length 3155 ); 3156 ctx.stroke(); 3157 } 3158 } 3159 3160 function drawPlasma(p) { 3161 p.pulse += 0.15; 3162 const pulseSize = p.size + Math.sin(p.pulse) * 4; 3163 3164 ctx.shadowBlur = p.glow; 3165 ctx.shadowColor = p.color; 3166 3167 // Multiple layers 3168 for (let i = 0; i < 3; i++) { 3169 ctx.fillStyle = p.color; 3170 ctx.globalAlpha = (1 - i * 0.3) * p.life; 3171 ctx.beginPath(); 3172 ctx.arc(0, 0, pulseSize - i * 4, 0, Math.PI * 2); 3173 ctx.fill(); 3174 } 3175 ctx.globalAlpha = 1; 3176 } 3177
3178 function drawVortex(p) { 3179 ctx.fillStyle = p.color; 3180 ctx.shadowBlur = 8; 3181 ctx.shadowColor = p.color; 3182 ctx.beginPath(); 3183 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 3184 ctx.fill(); 3185 } 3186 3187 function drawCosmicDust(p) { 3188 p.twinkle += 0.2; 3189 const brightness = 0.5 + Math.sin(p.twinkle) * 0.5; 3190 3191 ctx.fillStyle = p.color; 3192 ctx.shadowBlur = 5 * brightness; 3193 ctx.shadowColor = p.color; 3194 ctx.globalAlpha = brightness * p.life; 3195 ctx.beginPath(); 3196 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 3197 ctx.fill(); 3198 ctx.globalAlpha = 1; 3199 } 3200 3201 // ==================================== 3202 // NEW DRAW FUNCTIONS - PHYSICS & OCEAN 3203 // ==================================== 3204 3205 function drawBouncyBall(p) { 3206 ctx.fillStyle = p.color; 3207 ctx.shadowBlur = 8; 3208 ctx.shadowColor = p.color; 3209 ctx.beginPath(); 3210 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 3211 ctx.fill(); 3212 3213 // Highlight 3214 ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'; 3215 ctx.beginPath(); 3216 ctx.arc(-p.size * 0.3, -p.size * 0.3, p.size * 0.3, 0, Math.PI * 2); 3217 ctx.fill(); 3218 } 3219 3220 function drawPhysicsBox(p) { 3221 ctx.fillStyle = p.color; 3222 ctx.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.3})`; 3223 ctx.lineWidth = 2; 3224 ctx.shadowBlur = 5; 3225 ctx.shadowColor = p.color; 3226 3227 ctx.fillRect(-p.size/2, -p.size/2, p.size, p.size); 3228 ctx.strokeRect(-p.size/2, -p.size/2, p.size, p.size); 3229 } 3230 3231 function drawJellyfish(p) { 3232 p.pulse += 0.05 * p.pulseSpeed; 3233 const pulseSize = p.size * (1 + Math.sin(p.pulse) * 0.1); 3234 3235 if (p.image && p.image.complete) { 3236 ctx.save(); 3237 ctx.globalAlpha = p.life * 0.8; 3238 3239 // Apply hue rotation for color variation 3240 const hue = (p.hue + Date.now() * 0.01) % 360; 3241 ctx.filter = `hue-rotate(${hue}deg) saturate(0.7) brightness(1.2)`; 3242 3243 ctx.drawImage(p.image, -pulseSize/2, -pulseSize/2, pulseSize, pulseSize); 3244 ctx.restore(); 3245 } else { 3246 // Fallback: draw simple jellyfish shape 3247 ctx.fillStyle = `rgba(200, 150, 255, ${p.life * 0.6})`; 3248 ctx.beginPath(); 3249 ctx.ellipse(0, 0, pulseSize * 0.5, pulseSize * 0.6, 0, 0, Math.PI * 2); 3250 ctx.fill(); 3251 3252 // Tentacles 3253 ctx.strokeStyle = `rgba(200, 150, 255, ${p.life * 0.4})`; 3254 ctx.lineWidth = 2; 3255 for (let i = 0; i < 6; i++) { 3256 const angle = (i * Math.PI * 2) / 6; 3257 ctx.beginPath(); 3258 ctx.moveTo(Math.cos(angle) * pulseSize * 0.3, Math.sin(angle) * pulseSize * 0.3); 3259 ctx.lineTo(Math.cos(angle) * pulseSize * 0.6, Math.sin(angle) * pulseSize * 0.6 + pulseSize * 0.3); 3260 ctx.stroke(); 3261 } 3262 } 3263 } 3264 3265 function drawFish(p) { 3266 p.tailWag += 0.3; 3267 const tailOffset = Math.sin(p.tailWag) * 5; 3268 3269 ctx.fillStyle = p.color; 3270 3271 // Body 3272 ctx.beginPath(); 3273 ctx.ellipse(0, 0, p.size * 0.6, p.size * 0.4, 0, 0, Math.PI * 2); 3274 ctx.fill(); 3275 3276 // Tail 3277 ctx.beginPath(); 3278 ctx.moveTo(-p.size * 0.3, 0); 3279 ctx.lineTo(-p.size * 0.8 + tailOffset * p.direction, -p.size * 0.3); 3280 ctx.lineTo(-p.size * 0.8, 0); 3281 ctx.lineTo(-p.size * 0.8 + tailOffset * p.direction, p.size * 0.3); 3282 ctx.closePath(); 3283 ctx.fill(); 3284 3285 // Eye 3286 ctx.fillStyle = '#fff'; 3287 ctx.beginPath(); 3288 ctx.arc(p.size * 0.2, -p.size * 0.1, p.size * 0.1, 0, Math.PI * 2); 3289 ctx.fill(); 3290 ctx.fillStyle = '#000'; 3291 ctx.beginPath(); 3292 ctx.arc(p.size * 0.25, -p.size * 0.1, p.size * 0.05, 0, Math.PI * 2); 3293 ctx.fill(); 3294 } 3295 3296 function drawAcorn(p) { 3297 ctx.fillStyle = '#8b4513'; 3298 3299 // Cap 3300 ctx.beginPath(); 3301 ctx.arc(0, -p.size * 0.4, p.size * 0.3, 0, Math.PI * 2); 3302 ctx.fill(); 3303 3304 // Body 3305 ctx.beginPath(); 3306 ctx.ellipse(0, p.size * 0.2, p.size * 0.4, p.size * 0.5, 0, 0, Math.PI * 2); 3307 ctx.fill(); 3308 3309 // Stem 3310 ctx.strokeStyle = '#654321'; 3311 ctx.lineWidth = 2; 3312 ctx.beginPath(); 3313 ctx.moveTo(0, -p.size * 0.6); 3314 ctx.lineTo(0, -p.size * 0.4); 3315 ctx.stroke(); 3316 } 3317 3318 function drawPinecone(p) { 3319 ctx.fillStyle = '#8b6914'; 3320 3321 // Draw pinecone shape (oval with scales) 3322 ctx.beginPath(); 3323 ctx.ellipse(0, 0, p.size * 0.5, p.size * 0.7, 0, 0, Math.PI * 2); 3324 ctx.fill(); 3325 3326 // Scales 3327 ctx.strokeStyle = '#654321'; 3328 ctx.lineWidth = 1; 3329 for (let i = 0; i < 8; i++) { 3330 const angle = (i * Math.PI * 2) / 8; 3331 const x = Math.cos(angle) * p.size * 0.3; 3332 const y = Math.sin(angle) * p.size * 0.4; 3333 ctx.beginPath(); 3334 ctx.moveTo(0, 0); 3335 ctx.lineTo(x, y); 3336 ctx.stroke(); 3337 } 3338 } 3339 3340 function drawRaindrop(p) { 3341 if (p.splashY !== null) { 3342 // Draw splash 3343 const splashSize = (1 - p.life) * 15; 3344 ctx.strokeStyle = `rgba(150, 200, 255, ${p.life})`; 3345 ctx.lineWidth = 2; 3346 for (let i = 0; i < 4; i++) { 3347 const angle = (i * Math.PI * 2) / 4; 3348 ctx.beginPath(); 3349 ctx.moveTo(0, 0); 3350 ctx.lineTo(Math.cos(angle) * splashSize, Math.sin(angle) * splashSize); 3351 ctx.stroke(); 3352 } 3353 } else { 3354 // Draw raindrop 3355 ctx.fillStyle = `rgba(150, 200, 255, ${p.life})`; 3356 ctx.beginPath(); 3357 ctx.moveTo(0, -p.size); 3358 ctx.lineTo(-p.size * 0.3, 0); 3359 ctx.quadraticCurveTo(0, p.size * 0.3, p.size * 0.3, 0); 3360 ctx.closePath(); 3361 ctx.fill(); 3362 } 3363 } 3364 3365 function drawMist(p) { 3366 p.expansion += 0.01; 3367 const expandedSize = p.size * p.expansion; 3368 3369 ctx.fillStyle = `rgba(200, 200, 220, ${p.opacity * p.life})`; 3370 ctx.beginPath(); 3371 ctx.arc(0, 0, expandedSize, 0, Math.PI * 2); 3372 ctx.fill(); 3373 } 3374 3375 function drawFog(p) { 3376 p.expansion += 0.008; 3377 const expandedSize = p.size * p.expansion; 3378 3379 ctx.fillStyle = `rgba(180, 180, 200, ${p.opacity * p.life})`; 3380 ctx.beginPath(); 3381 ctx.arc(0, 0, expandedSize, 0, Math.PI * 2); 3382 ctx.fill(); 3383 3384 // Additional layers for fog effect 3385 ctx.fillStyle = `rgba(160, 160, 180, ${p.opacity * p.life * 0.5})`; 3386 ctx.beginPath(); 3387 ctx.arc(expandedSize * 0.3, expandedSize * 0.2, expandedSize * 0.6, 0, Math.PI * 2); 3388 ctx.fill(); 3389 } 3390 3391 function drawChalk(p) { 3392 p.smudge += 0.05; 3393 const smudgeSize = p.size * (1 + p.smudge * 0.3); 3394 3395 ctx.fillStyle = p.color; 3396 ctx.globalAlpha = p.life * 0.7; 3397 3398 // Main mark 3399 ctx.beginPath(); 3400 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 3401 ctx.fill(); 3402 3403 // Smudge effect 3404 ctx.globalAlpha = p.life * 0.3; 3405 ctx.beginPath(); 3406 ctx.arc(0, 0, smudgeSize, 0, Math.PI * 2); 3407 ctx.fill(); 3408 ctx.globalAlpha = 1; 3409 } 3410 3411 function drawPastel(p) { 3412 p.blend += 0.02; 3413 3414 // Create gradient for pastel effect 3415 const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, p.size); 3416 gradient.addColorStop(0, p.color); 3417 gradient.addColorStop(0.5, p.color.replace(')', ', 0.6)').replace('rgb', 'rgba').replace('hsl', 'hsla')); 3418 gradient.addColorStop(1, p.color.replace(')', ', 0.2)').replace('rgb', 'rgba').replace('hsl', 'hsla')); 3419 3420 ctx.fillStyle = gradient; 3421 ctx.beginPath(); 3422 ctx.arc(0, 0, p.size * (1 + p.blend * 0.2), 0, Math.PI * 2); 3423 ctx.fill(); 3424 } 3425 3426 function drawDataPacket(p) { 3427 // Track path 3428 p.path.push({x: 0, y: 0}); 3429 if (p.path.length > 10) p.path.shift(); 3430 3431 // Draw path 3432 ctx.strokeStyle = `rgba(0, 255, 0, ${p.life * 0.3})`; 3433 ctx.lineWidth = 1; 3434 ctx.beginPath(); 3435 for (let i = 0; i < p.path.length; i++) { 3436 const point = p.path[i]; 3437 if (i === 0) ctx.moveTo(point.x, point.y); 3438 else ctx.lineTo(point.x, point.y); 3439 } 3440 ctx.stroke(); 3441 3442 // Draw packet 3443 ctx.fillStyle = p.color; 3444 ctx.fillRect(-p.size/2, -p.size/2, p.size, p.size); 3445 ctx.strokeStyle = '#0f0'; 3446 ctx.lineWidth = 1; 3447 ctx.strokeRect(-p.size/2, -p.size/2, p.size, p.size); 3448 3449 // Draw data text 3450 ctx.fillStyle = '#000'; 3451 ctx.font = `${p.size * 0.4}px monospace`; 3452 ctx.textAlign = 'center'; 3453 ctx.textBaseline = 'middle'; 3454 ctx.fillText(p.data, 0, 0); 3455 } 3456 3457 function drawSoundWave(p) { 3458 const progress = 1 - p.life; 3459 const currentSize = p.size + (p.maxSize - p.size) * progress; 3460 p.phase += p.frequency; 3461 3462 ctx.strokeStyle = p.color; 3463 ctx.lineWidth = 2; 3464 ctx.globalAlpha = p.life; 3465 3466 // Draw wave pattern 3467 ctx.beginPath(); 3468 for (let i = 0; i < currentSize; i += 2) { 3469 const angle = (i / currentSize) * Math.PI * 2; 3470 const radius = currentSize * 0.3 + Math.sin(p.phase + i * 0.1) * currentSize * 0.1; 3471 const x = Math.cos(angle) * radius; 3472 const y = Math.sin(angle) * radius; 3473 if (i === 0) ctx.moveTo(x, y); 3474 else ctx.lineTo(x, y); 3475 } 3476 ctx.closePath(); 3477 ctx.stroke(); 3478 3479 ctx.globalAlpha = 1; 3480 } 3481 3482 function drawTrafficLight(p) { 3483 p.cycleTime += 0.02; 3484 if (p.cycleTime >= 1) { 3485 p.cycleTime = 0; 3486 p.state = (p.state + 1) % 3; 3487 } 3488 3489 // Draw housing 3490 ctx.fillStyle = '#333'; 3491 ctx.fillRect(-p.size/2, -p.size * 0.8, p.size, p.size * 1.6); 3492 3493 // Draw lights 3494 const lights = ['#f00', '#ff0', '#0f0']; 3495 const lightSize = p.size * 0.3; 3496 const spacing = p.size * 0.5; 3497 3498 for (let i = 0; i < 3; i++) { 3499 const y = -p.size * 0.6 + i * spacing; 3500 ctx.fillStyle = i === p.state ? lights[i] : '#333'; 3501 ctx.beginPath(); 3502 ctx.arc(0, y, lightSize, 0, Math.PI * 2); 3503 ctx.fill(); 3504 3505 if (i === p.state) { 3506 ctx.shadowBlur = 15; 3507 ctx.shadowColor = lights[i]; 3508 ctx.fill(); 3509 ctx.shadowBlur = 0; 3510 } 3511 } 3512 } 3513 3514 function drawBee(p) { 3515 p.wingFlap += 0.5; 3516 p.zigzag += 0.2;
3517 const zigzagOffset = Math.sin(p.zigzag) * 3; 3518 3519 ctx.translate(zigzagOffset, 0); 3520 3521 // Body 3522 ctx.fillStyle = p.color; 3523 ctx.beginPath(); 3524 ctx.ellipse(0, 0, p.size * 0.6, p.size * 0.4, 0, 0, Math.PI * 2); 3525 ctx.fill(); 3526 3527 // Stripes 3528 ctx.fillStyle = '#000'; 3529 for (let i = 0; i < 3; i++) { 3530 ctx.fillRect(-p.size * 0.5, -p.size * 0.3 + i * p.size * 0.2, p.size, p.size * 0.1); 3531 } 3532 3533 // Wings 3534 const flapAngle = Math.sin(p.wingFlap) * 0.3; 3535 ctx.fillStyle = 'rgba(200, 200, 255, 0.6)'; 3536 ctx.save(); 3537 ctx.rotate(flapAngle); 3538 ctx.beginPath(); 3539 ctx.ellipse(p.size * 0.3, -p.size * 0.2, p.size * 0.3, p.size * 0.2, 0, 0, Math.PI * 2); 3540 ctx.fill(); 3541 ctx.restore(); 3542 3543 ctx.save(); 3544 ctx.rotate(-flapAngle); 3545 ctx.beginPath(); 3546 ctx.ellipse(p.size * 0.3, p.size * 0.2, p.size * 0.3, p.size * 0.2, 0, 0, Math.PI * 2); 3547 ctx.fill(); 3548 ctx.restore(); 3549 } 3550 3551 function drawLensFlare(p) { 3552 p.rotation += p.rotationSpeed; 3553 3554 ctx.save(); 3555 ctx.rotate(p.rotation); 3556 3557 // Helper function to adjust alpha in rgba color 3558 function adjustAlpha(colorStr, newAlpha) { 3559 // Handle rgba format 3560 if (colorStr.startsWith('rgba(')) { 3561 return colorStr.replace(/rgba\(([^)]+)\)/, (match, values) => { 3562 const parts = values.split(',').map(v => v.trim()); 3563 if (parts.length === 4) { 3564 return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${newAlpha})`; 3565 } 3566 return match; 3567 }); 3568 } 3569 // Handle rgb format 3570 if (colorStr.startsWith('rgb(')) { 3571 return colorStr.replace('rgb(', 'rgba(').replace(')', `, ${newAlpha})`); 3572 } 3573 // Handle hsl/hsla format 3574 if (colorStr.startsWith('hsla(')) { 3575 return colorStr.replace(/hsla\(([^)]+)\)/, (match, values) => { 3576 const parts = values.split(',').map(v => v.trim()); 3577 if (parts.length === 4) { 3578 return `hsla(${parts[0]}, ${parts[1]}, ${parts[2]}, ${newAlpha})`; 3579 } 3580 return match; 3581 }); 3582 } 3583 if (colorStr.startsWith('hsl(')) { 3584 return colorStr.replace('hsl(', 'hsla(').replace(')', `, ${newAlpha})`); 3585 } 3586 // Fallback: wrap in rgba 3587 return `rgba(255, 255, 255, ${newAlpha})`; 3588 } 3589 3590 // Main flare 3591 const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, p.size); 3592 gradient.addColorStop(0, p.color); 3593 gradient.addColorStop(0.5, adjustAlpha(p.color, 0.5)); 3594 gradient.addColorStop(1, adjustAlpha(p.color, 0)); 3595 3596 ctx.fillStyle = gradient; 3597 ctx.beginPath(); 3598 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 3599 ctx.fill(); 3600 3601 // Rays 3602 ctx.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.3})`; 3603 ctx.lineWidth = 2; 3604 for (let i = 0; i < 8; i++) { 3605 const angle = (i * Math.PI * 2) / 8; 3606 ctx.beginPath(); 3607 ctx.moveTo(0, 0); 3608 ctx.lineTo(Math.cos(angle) * p.size, Math.sin(angle) * p.size); 3609 ctx.stroke(); 3610 } 3611 3612 ctx.restore(); 3613 } 3614 3615 function drawPrism(p) { 3616 p.lightAngle += 0.05; 3617 3618 // Draw triangular prism 3619 ctx.fillStyle = `rgba(200, 200, 255, ${p.life * 0.6})`; 3620 ctx.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.8})`; 3621 ctx.lineWidth = 2; 3622 3623 ctx.beginPath(); 3624 ctx.moveTo(0, -p.size); 3625 ctx.lineTo(-p.size * 0.6, p.size * 0.5); 3626 ctx.lineTo(p.size * 0.6, p.size * 0.5); 3627 ctx.closePath(); 3628 ctx.fill(); 3629 ctx.stroke(); 3630 3631 // Refracted light (rainbow effect) 3632 if (typeof gsap !== 'undefined') { 3633 // Use GSAP for smooth color transitions if available 3634 const hue = (p.lightAngle * 57.3) % 360; 3635 ctx.fillStyle = `hsla(${hue}, 70%, 60%, ${p.life * 0.4})`; 3636 } else { 3637 // Fallback: simple rainbow gradient 3638 const gradient = ctx.createLinearGradient(-p.size, 0, p.size, 0); 3639 gradient.addColorStop(0, `hsla(${(p.lightAngle * 57.3) % 360}, 70%, 60%, ${p.life * 0.4})`); 3640 gradient.addColorStop(0.5, `hsla(${((p.lightAngle * 57.3) + 60) % 360}, 70%, 60%, ${p.life * 0.4})`); 3641 gradient.addColorStop(1, `hsla(${((p.lightAngle * 57.3) + 120) % 360}, 70%, 60%, ${p.life * 0.4})`); 3642 ctx.fillStyle = gradient; 3643 } 3644 3645 // Draw refracted light rays 3646 for (let i = 0; i < 3; i++) { 3647 const angle = p.lightAngle + (i - 1) * 0.3; 3648 ctx.beginPath(); 3649 ctx.moveTo(0, -p.size); 3650 ctx.lineTo(Math.cos(angle) * p.size * 1.5, Math.sin(angle) * p.size * 1.5); 3651 ctx.stroke(); 3652 } 3653 } 3654 3655 // ==================================== 3656 // NEW DRAW FUNCTIONS â latest trails 3657 // ==================================== 3658 3659 function drawChainLink(p) { 3660 if (p.parent && p.parent.life > 0) { 3661 const dx = p.parent.x - p.x; 3662 const dy = p.parent.y - p.y; 3663 ctx.save(); 3664 if (p.rotation) ctx.rotate(-p.rotation); 3665 ctx.strokeStyle = `rgba(80, 60, 40, ${p.life * 0.5})`; 3666 ctx.lineWidth = 2; 3667 ctx.beginPath(); 3668 ctx.moveTo(dx, dy); 3669 ctx.lineTo(0, 0); 3670 ctx.stroke();
3671 ctx.restore(); 3672 } 3673 ctx.fillStyle = p.color; 3674 ctx.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.25})`; 3675 ctx.lineWidth = 1.5; 3676 ctx.beginPath(); 3677 ctx.ellipse(0, 0, p.size * 0.55, p.size * 0.85, 0, 0, Math.PI * 2); 3678 ctx.fill(); 3679 ctx.stroke(); 3680 ctx.fillStyle = `rgba(255, 255, 255, ${p.life * 0.2})`; 3681 ctx.beginPath(); 3682 ctx.ellipse(0, -p.size * 0.15, p.size * 0.25, p.size * 0.35, 0, 0, Math.PI * 2); 3683 ctx.fill(); 3684 } 3685 3686 function drawDomino(p) { 3687 const w = p.width; 3688 const h = p.height; 3689 ctx.fillStyle = p.color; 3690 ctx.strokeStyle = `rgba(0, 0, 0, ${p.life * 0.35})`; 3691 ctx.lineWidth = 1; 3692 ctx.fillRect(-w / 2, -h / 2, w, h); 3693 ctx.strokeRect(-w / 2, -h / 2, w, h); 3694 ctx.fillStyle = `rgba(30, 30, 30, ${p.life * 0.85})`; 3695 const pipR = Math.max(1.2, w * 0.18); 3696 const pipPositions = { 3697 1: [[0, 0]], 3698 2: [[-0.25, -0.25], [0.25, 0.25]], 3699 3: [[-0.25, -0.25], [0, 0], [0.25, 0.25]], 3700 4: [[-0.25, -0.25], [0.25, -0.25], [-0.25, 0.25], [0.25, 0.25]], 3701 5: [[-0.25, -0.25], [0.25, -0.25], [0, 0], [-0.25, 0.25], [0.25, 0.25]], 3702 6: [[-0.25, -0.3], [0.25, -0.3], [-0.25, 0], [0.25, 0], [-0.25, 0.3], [0.25, 0.3]] 3703 }; 3704 const positions = pipPositions[p.dots] || pipPositions[1]; 3705 positions.forEach(function (pos) { 3706 ctx.beginPath(); 3707 ctx.arc(pos[0] * w, pos[1] * h, pipR, 0, Math.PI * 2); 3708 ctx.fill(); 3709 }); 3710 } 3711 3712 function drawSeahorse(p) { 3713 p.swimPhase += 0.08; 3714 const curl = Math.sin(p.swimPhase) * 0.15; 3715 ctx.scale(p.direction, 1); 3716 ctx.fillStyle = p.color; 3717 ctx.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.2})`; 3718 ctx.lineWidth = 1; 3719 ctx.beginPath(); 3720 ctx.moveTo(p.size * 0.4, 0); 3721 ctx.quadraticCurveTo(p.size * 0.1, -p.size * 0.5, -p.size * 0.2, -p.size * 0.15); 3722 ctx.quadraticCurveTo(-p.size * 0.55 + curl * p.size, p.size * 0.35, -p.size * 0.35, p.size * 0.55); 3723 ctx.quadraticCurveTo(-p.size * 0.1, p.size * 0.75, p.size * 0.15, p.size * 0.35); 3724 ctx.quadraticCurveTo(p.size * 0.35, p.size * 0.1, p.size * 0.4, 0); 3725 ctx.fill(); 3726 ctx.stroke(); 3727 ctx.fillStyle = '#fff'; 3728 ctx.beginPath(); 3729 ctx.arc(p.size * 0.32, -p.size * 0.08, p.size * 0.07, 0, Math.PI * 2); 3730 ctx.fill(); 3731 ctx.fillStyle = '#111'; 3732 ctx.beginPath(); 3733 ctx.arc(p.size * 0.34, -p.size * 0.08, p.size * 0.035, 0, Math.PI * 2); 3734 ctx.fill(); 3735 } 3736 3737 function drawStarfish(p) { 3738 ctx.fillStyle = p.color; 3739 ctx.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.15})`; 3740 ctx.lineWidth = 1; 3741 ctx.beginPath(); 3742 for (let i = 0; i < p.arms * 2; i++) { 3743 const angle = (i * Math.PI) / p.arms - Math.PI / 2; 3744 const radius = i % 2 === 0 ? p.size : p.size * 0.45; 3745 const px = Math.cos(angle) * radius; 3746 const py = Math.sin(angle) * radius; 3747 if (i === 0) ctx.moveTo(px, py); 3748 else ctx.lineTo(px, py); 3749 } 3750 ctx.closePath(); 3751 ctx.fill(); 3752 ctx.stroke(); 3753 } 3754 3755 function drawCoral(p) { 3756 ctx.strokeStyle = p.color; 3757 ctx.lineWidth = 2.5; 3758 ctx.lineCap = 'round'; 3759 (p.branchData || []).forEach(function (branch) { 3760 const len = p.size * branch.len; 3761 ctx.beginPath(); 3762 ctx.moveTo(0, 0); 3763 ctx.quadraticCurveTo( 3764 Math.cos(branch.angle) * len * 0.4, 3765 Math.sin(branch.angle) * len * 0.4 - p.size * 0.2, 3766 Math.cos(branch.angle) * len, 3767 Math.sin(branch.angle) * len 3768 ); 3769 ctx.stroke(); 3770 }); 3771 ctx.fillStyle = `rgba(255, 255, 255, ${p.life * 0.25})`; 3772 ctx.beginPath(); 3773 ctx.arc(0, 0, p.size * 0.15, 0, Math.PI * 2); 3774 ctx.fill(); 3775 } 3776 3777 function drawMushroom(p) { 3778 const scale = p.grow; 3779 const capR = p.size * scale; 3780 ctx.fillStyle = p.stemColor; 3781 ctx.fillRect(-capR * 0.2, 0, capR * 0.4, capR * 0.7); 3782 ctx.fillStyle = p.capColor; 3783 ctx.beginPath(); 3784 ctx.arc(0, 0, capR * 0.65, Math.PI, 0); 3785 ctx.fill(); 3786 if (p.spots) { 3787 ctx.fillStyle = `rgba(255, 255, 255, ${p.life * 0.7})`; 3788 for (let i = 0; i < 3; i++) { 3789 const sx = (i - 1) * capR * 0.25; 3790 ctx.beginPath(); 3791 ctx.arc(sx, -capR * 0.2, capR * 0.1, 0, Math.PI * 2); 3792 ctx.fill(); 3793 } 3794 } 3795 } 3796 3797 function drawFeather(p) { 3798 ctx.fillStyle = p.color; 3799 ctx.strokeStyle = `rgba(180, 180, 200, ${p.life * 0.4})`; 3800 ctx.lineWidth = 1; 3801 ctx.beginPath(); 3802 ctx.moveTo(0, -p.size * 0.5); 3803 ctx.quadraticCurveTo(p.size * 0.35, 0, 0, p.size * 0.5); 3804 ctx.quadraticCurveTo(-p.size * 0.35, 0, 0, -p.size * 0.5); 3805 ctx.fill(); 3806 ctx.stroke(); 3807 ctx.beginPath(); 3808 ctx.moveTo(0, -p.size * 0.45); 3809 ctx.lineTo(0, p.size * 0.45); 3810 ctx.stroke(); 3811 for (let i = -3; i <= 3; i++) { 3812 if (i === 0) continue; 3813 ctx.beginPath(); 3814 ctx.moveTo(0, i * p.size * 0.1); 3815 ctx.lineTo(p.size * 0.2, i * p.size * 0.12); 3816 ctx.stroke(); 3817 } 3818 } 3819 3820 function drawFruit(p) { 3821 ctx.fillStyle = p.color; 3822 ctx.beginPath(); 3823 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 3824 ctx.fill(); 3825 if (p.fruitKind === 'berry' || p.fruitKind === 'grape') { 3826 ctx.fillStyle = `rgba(255, 255, 255, ${p.life * 0.35})`; 3827 ctx.beginPath(); 3828 ctx.arc(-p.size * 0.25, -p.size * 0.25, p.size * 0.2, 0, Math.PI * 2); 3829 ctx.fill(); 3830 } 3831 ctx.strokeStyle = p.stemColor; 3832 ctx.lineWidth = 2; 3833 ctx.beginPath(); 3834 ctx.moveTo(0, -p.size); 3835 ctx.lineTo(p.size * 0.1, -p.size * 1.3); 3836 ctx.stroke(); 3837 } 3838 3839 function drawPortal(p) { 3840 const pulseSize = p.size * (1 + Math.sin(p.pulse) * 0.12); 3841 ctx.strokeStyle = `hsla(${p.innerHue}, 90%, 65%, ${p.life * 0.9})`; 3842 ctx.lineWidth = 3; 3843 ctx.shadowBlur = 12; 3844 ctx.shadowColor = `hsl(${p.innerHue}, 90%, 60%)`; 3845 ctx.beginPath(); 3846 ctx.ellipse(0, 0, pulseSize, pulseSize * 0.75, 0, 0, Math.PI * 2); 3847 ctx.stroke(); 3848 ctx.strokeStyle = `hsla(${(p.innerHue + 60) % 360}, 85%, 70%, ${p.life * 0.5})`; 3849 ctx.lineWidth = 1.5; 3850 ctx.beginPath(); 3851 ctx.ellipse(0, 0, pulseSize * 0.65, pulseSize * 0.5, 0, 0, Math.PI * 2); 3852 ctx.stroke(); 3853 ctx.fillStyle = `hsla(${p.innerHue}, 100%, 75%, ${p.life * 0.25})`; 3854 ctx.beginPath(); 3855 ctx.arc(0, 0, pulseSize * 0.2, 0, Math.PI * 2); 3856 ctx.fill(); 3857 ctx.shadowBlur = 0; 3858 } 3859 3860 function drawManaOrb(p) { 3861 const pulseSize = p.size + Math.sin(p.pulse) * 3; 3862 ctx.shadowBlur = 14; 3863 ctx.shadowColor = p.color; 3864 const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, pulseSize); 3865 gradient.addColorStop(0, '#fff'); 3866 gradient.addColorStop(0.4, p.color); 3867 gradient.addColorStop(1, 'rgba(0,0,0,0)'); 3868 ctx.fillStyle = gradient; 3869 ctx.beginPath(); 3870 ctx.arc(0, 0, pulseSize, 0, Math.PI * 2); 3871 ctx.fill(); 3872 ctx.shadowBlur = 0; 3873 } 3874 3875 function drawVinylRecord(p) { 3876 ctx.fillStyle = '#1a1a1a'; 3877 ctx.beginPath(); 3878 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 3879 ctx.fill(); 3880 ctx.strokeStyle = `rgba(255, 255, 255, ${p.life * 0.08})`; 3881 ctx.lineWidth = 0.5; 3882 for (let r = p.size * 0.3; r < p.size; r += p.size * 0.12) { 3883 ctx.beginPath(); 3884 ctx.arc(0, 0, r, 0, Math.PI * 2); 3885 ctx.stroke(); 3886 } 3887 ctx.fillStyle = `hsl(${p.labelHue}, 70%, 55%)`; 3888 ctx.beginPath(); 3889 ctx.arc(0, 0, p.size * 0.32, 0, Math.PI * 2); 3890 ctx.fill(); 3891 ctx.fillStyle = '#111'; 3892 ctx.beginPath(); 3893 ctx.arc(0, 0, p.size * 0.06, 0, Math.PI * 2); 3894 ctx.fill(); 3895 } 3896 3897 function drawCoffeeBean(p) { 3898 ctx.fillStyle = p.color; 3899 ctx.beginPath(); 3900 ctx.ellipse(0, 0, p.size * 0.7, p.size, 0, 0, Math.PI * 2); 3901 ctx.fill(); 3902 ctx.strokeStyle = `rgba(0, 0, 0, ${p.life * 0.4})`; 3903 ctx.lineWidth = 1; 3904 ctx.beginPath(); 3905 ctx.moveTo(0, -p.size); 3906 ctx.quadraticCurveTo(p.size * 0.35, 0, 0, p.size); 3907 ctx.stroke(); 3908 } 3909 3910 function drawSprinkle(p) { 3911 ctx.fillStyle = p.color; 3912 ctx.fillRect(-p.size / 2, -p.length / 2, p.size, p.length); 3913 ctx.fillStyle = `rgba(255, 255, 255, ${p.life * 0.35})`; 3914 ctx.fillRect(-p.size / 4, -p.length / 2, p.size / 2, p.length * 0.4); 3915 } 3916 3917 function drawDice(p) { 3918 const s = p.size; 3919 ctx.fillStyle = p.color; 3920 ctx.strokeStyle = `rgba(0, 0, 0, ${p.life * 0.3})`; 3921 ctx.lineWidth = 1.5; 3922 ctx.beginPath(); 3923 if (typeof ctx.roundRect === 'function') { 3924 ctx.roundRect(-s / 2, -s / 2, s, s, s * 0.15); 3925 } else { 3926 ctx.rect(-s / 2, -s / 2, s, s); 3927 } 3928 ctx.fill(); 3929 ctx.stroke(); 3930 ctx.fillStyle = p.pipColor; 3931 const pipR = s * 0.1; 3932 const pipMap = { 3933 1: [[0, 0]], 3934 2: [[-0.28, -0.28], [0.28, 0.28]], 3935 3: [[-0.28, -0.28], [0, 0], [0.28, 0.28]], 3936 4: [[-0.28, -0.28], [0.28, -0.28], [-0.28, 0.28], [0.28, 0.28]], 3937 5: [[-0.28, -0.28], [0.28, -0.28], [0, 0], [-0.28, 0.28], [0.28, 0.28]], 3938 6: [[-0.28, -0.32], [0.28, -0.32], [-0.28, 0], [0.28, 0], [-0.28, 0.32], [0.28, 0.32]] 3939 };
3940 (pipMap[p.face] || pipMap[1]).forEach(function (pos) { 3941 ctx.beginPath(); 3942 ctx.arc(pos[0] * s, pos[1] * s, pipR, 0, Math.PI * 2); 3943 ctx.fill(); 3944 }); 3945 } 3946 3947 function drawChatBubble(p) { 3948 const padX = 8; 3949 const padY = 5; 3950 ctx.font = `bold ${Math.max(10, p.size * 0.55)}px Tahoma, Verdana, sans-serif`; 3951 const textW = ctx.measureText(p.text).width; 3952 const bw = textW + padX * 2; 3953 const bh = p.size + padY * 2; 3954 const hue = p.bubbleHue; 3955 3956 ctx.fillStyle = `hsla(${hue}, 55%, 92%, ${p.life * 0.95})`; 3957 ctx.strokeStyle = `hsla(${hue}, 60%, 45%, ${p.life * 0.7})`; 3958 ctx.lineWidth = 1.5; 3959 ctx.beginPath(); 3960 if (typeof ctx.roundRect === 'function') { 3961 ctx.roundRect(-bw / 2, -bh / 2, bw, bh, 6); 3962 } else { 3963 ctx.rect(-bw / 2, -bh / 2, bw, bh); 3964 } 3965 ctx.fill(); 3966 ctx.stroke(); 3967 ctx.beginPath(); 3968 ctx.moveTo(-bw * 0.15, bh / 2); 3969 ctx.lineTo(-bw * 0.05, bh / 2 + 6); 3970 ctx.lineTo(bw * 0.05, bh / 2); 3971 ctx.fill(); 3972 3973 ctx.fillStyle = `hsla(${hue}, 50%, 25%, ${p.life})`; 3974 ctx.textAlign = 'center'; 3975 ctx.textBaseline = 'middle'; 3976 ctx.fillText(p.text, 0, 0); 3977 } 3978 3979 function drawSpaghettiSauce(p) { 3980 const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, p.size); 3981 gradient.addColorStop(0, `rgba(192, 57, 43, ${p.life * 0.85})`); 3982 gradient.addColorStop(0.6, `rgba(160, 40, 30, ${p.life * 0.6})`); 3983 gradient.addColorStop(1, `rgba(120, 30, 20, 0)`); 3984 ctx.fillStyle = gradient; 3985 ctx.beginPath(); 3986 ctx.arc(0, 0, p.size, 0, Math.PI * 2); 3987 ctx.fill(); 3988 } 3989 3990 // ==================================== 3991 // PUBLIC API 3992 // ==================================== 3993 3994 function setTrailColors(colors) { 3995 trailColorOverrides = mergeTrailColors(colors); 3996 emitDevLog('⨠Trail colors updated', trailColorOverrides); 3997 } 3998 3999 function getTrailColors() { 4000 return mergeTrailColors(trailColorOverrides); 4001 } 4002 4003 function setTrailType(type) { 4004 if (TRAIL_TYPES[type] === undefined) { 4005 emitDevWarn(`Unknown trail type: ${type}`); 4006 return; 4007 } 4008 4009 currentTrailType = type; 4010 spaghettiPath = []; 4011 4012 if (window.PupwifeCursorTrailHandlers) { 4013 window.PupwifeCursorTrailHandlers.onTrailChange(type); 4014 } 4015 if (window.PupwifeCursorTrailsMatter) { 4016 if (isMatterTrail(type) && canvas) { 4017 window.PupwifeCursorTrailsMatter.onTrailChange(type, canvas.width, canvas.height); 4018 } else if (!isMatterTrail(type)) { 4019 window.PupwifeCursorTrailsMatter.destroy(); 4020 } 4021 } 4022 requestTrailExtensions(); 4023 4024 // Start/stop animation 4025 if (type === 'none') { 4026 if (animationFrameId !== null) { 4027 cancelAnimationFrame(animationFrameId); 4028 animationFrameId = null; 4029 } 4030 particles = []; 4031 spaghettiPath = []; 4032 if (window.PupwifeCursorTrailsMatter) { 4033 window.PupwifeCursorTrailsMatter.destroy(); 4034 } 4035 if (ctx) { 4036 ctx.clearRect(0, 0, canvas.width, canvas.height); 4037 } 4038 } else { 4039 if (animationFrameId === null) { 4040 update(); 4041 } 4042 } 4043 4044 emitDevLog(`⨠Cursor trail changed to: ${type}`); 4045 } 4046 4047 function setTrailAmount(level) { 4048 currentTrailAmount = normalizeTrailAmount(level); 4049 emitDevLog(`⨠Trail amount: ${TRAIL_AMOUNT_LABELS[currentTrailAmount]}`); 4050 } 4051 4052 function getTrailAmount() { 4053 return currentTrailAmount; 4054 } 4055 4056 function getTrailType() { 4057 return currentTrailType; 4058 } 4059 4060 function cleanup() { 4061 if (animationFrameId !== null) { 4062 cancelAnimationFrame(animationFrameId); 4063 } 4064 if (canvas && canvas.parentNode) { 4065 canvas.parentNode.removeChild(canvas); 4066 } 4067 document.removeEventListener('mousemove', handleMouseMove); 4068 window.removeEventListener('resize', resizeCanvas); 4069 } 4070 4071 // ==================================== 4072 // EXPORT 4073 // ==================================== 4074 4075 window.CursorTrails = { 4076 init, 4077 setTrailType, 4078 setTrailAmount, 4079 setTrailColors, 4080 getTrailColors, 4081 getTrailType, 4082 getTrailAmount, 4083 cleanup, 4084 TRAIL_TYPES,
4085 TRAIL_AMOUNT_LABELS 4086 }; 4087 4088 // Auto-initialize on load 4089 if (document.readyState === 'loading') { 4090 document.addEventListener('DOMContentLoaded', init); 4091 } else { 4092 init(); 4093 } 4094 4095})(); 4096
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.