1/** 2 * Jellypuff â 3D soft-body jellyfish cursor follower. 3 * Medusae WebGL body (particulate-medusae, Artistic-2.0) + Aurelia pulse-jet swim motion. 4 */ 5(function (global) { 6 'use strict'; 7 8 var PRIDE_PRESETS = { 9 rainbow: ['#e40303', '#ff8c00', '#ffed00', '#008026', '#24408e', '#732982'], 10 trans: ['#5BCEFA', '#F5A9B8', '#FFFFFF', '#F5A9B8', '#5BCEFA'], 11 bi: ['#D60270', '#9B4F96', '#0038A8'], 12 pan: ['#FF218C', '#FFD800', '#21B1FF'], 13 lesbian: ['#D52D00', '#FF9A56', '#FFFFFF', '#D362A4', '#A40062'], 14 nb: ['#FCF434', '#FFFFFF', '#9C59D1', '#2C2C2C'], 15 ace: ['#000000', '#A3A3A3', '#FFFFFF', '#810081'], 16 progress: ['#FFFFFF', '#FFAEC9', '#74D7EE', '#613915', '#000000'] 17 }; 18 19 var DEFAULT_BELL = '#0a4a68'; 20 var DEFAULT_BELL_ALT = '#3ec8e8'; 21 var DEFAULT_TENT = '#2a5878'; 22 var DEFAULT_GEL = '#6ee8ff'; 23 24 var BASE_SINK = 0.58; 25 var JET_PEAK = 1.48; 26 var DRIFT_RECOVERY = 0.38; 27 var CHARGE_GAIN = 0.38; 28 var TIME_RATE = 0.034; 29 var HORIZ_STEER = 0.078; 30 var HORIZ_DAMP = 0.885; 31 var MAX_HORIZ_SPEED = 0.92; 32 var CHASE_GAIN = 0.13; 33 var TILT_SMOOTH = 0.055; 34 var WOBBLE_AMP = 0.016; 35 var WOBBLE_RATE = 0.48; 36 var LEAN_SMOOTH = 0.035; 37 var LEAN_AMP = 0.038; 38 var PIN_JET_SCALE = 10; 39 var TRAVEL_LEAN = 0.1; 40 41 var container = null; 42 var renderer = null; 43 var scene = null; 44 var camera = null; 45 var medusae = null; 46 var looper = null; 47 var animId = null; 48 var isActive = false; 49 var config = null; 50 var mouse = { x: 0, y: 0 }; 51 var pos = { x: 0, y: 0 }; 52 var vel = { x: 0, y: 0 }; 53 var animTime = 0; 54 var wobbleSeed = Math.random() * 1000; 55 var charge = 0; 56 var smoothDist = 0; 57 var smoothBellPhase = 0; 58 var bellPhase = 0; 59 var renderTilt = 0; 60 var leanTilt = 0; 61 var lastBuildKey = ''; 62 63 function emitDevLog() { 64 if (global.devLog) global.devLog.apply(global, arguments); 65 } 66 67 function emitDevWarn() { 68 if (global.devWarn) global.devWarn.apply(global, arguments); 69 } 70 71 function defaultColorPart(solid) { 72 return { colorMode: 'solid', solidColor: solid, rgbSpeed: 1, pridePreset: 'rainbow' }; 73 } 74 75 function defaultConfig() { 76 return { 77 size: 100, 78 pulseRate: 100, 79 tentacleDensity: 100, 80 glowStrength: 100, 81 bell: defaultColorPart(DEFAULT_BELL), 82 tentacles: defaultColorPart(DEFAULT_TENT) 83 }; 84 } 85 86 function clampInt(v, min, max, fb) { 87 var n = parseInt(v, 10); 88 if (Number.isNaN(n)) return fb; 89 return Math.max(min, Math.min(max, n)); 90 } 91 92 function clampNum(v, min, max, fb) { 93 var n = parseFloat(v); 94 if (Number.isNaN(n)) return fb; 95 return Math.max(min, Math.min(max, n)); 96 } 97 98 function mergeColorPart(raw, fallback) { 99 if (!raw || typeof raw !== 'object') return Object.assign({}, fallback); 100 var mode = raw.colorMode; 101 if (mode !== 'solid' && mode !== 'rgb' && mode !== 'pride') mode = fallback.colorMode; 102 return { 103 colorMode: mode, 104 solidColor: typeof raw.solidColor === 'string' ? raw.solidColor : fallback.solidColor, 105 rgbSpeed: clampNum(raw.rgbSpeed, 0.2, 3, fallback.rgbSpeed), 106 pridePreset: PRIDE_PRESETS[raw.pridePreset] ? raw.pridePreset : fallback.pridePreset 107 }; 108 } 109 110 function mergeConfig(raw) { 111 var d = defaultConfig(); 112 if (!raw || typeof raw !== 'object') return d; 113 return { 114 size: clampInt(raw.size, 25, 200, d.size), 115 pulseRate: clampInt(raw.pulseRate, 40, 200, d.pulseRate), 116 tentacleDensity: clampInt(raw.tentacleDensity, 50, 200, d.tentacleDensity), 117 glowStrength: clampInt(raw.glowStrength, 0, 200, d.glowStrength), 118 bell: mergeColorPart(raw.bell, d.bell), 119 tentacles: mergeColorPart(raw.tentacles, d.tentacles) 120 }; 121 } 122 123 function readConfig() { 124 if (global.SettingsMenu && typeof global.SettingsMenu.getSetting === 'function') { 125 return mergeConfig(global.SettingsMenu.getSetting('jellypuff')); 126 } 127 return defaultConfig(); 128 } 129 130 function scaleFactor() { 131 return (config ? config.size : 100) / 100; 132 } 133 134 function densityFactor() { 135 return (config ? config.tentacleDensity : 100) / 100; 136 } 137 138 function glowFactor() { 139 return (config ? config.glowStrength : 100) / 100; 140 } 141 142 function hexToRgb(hex) { 143 var h = hex.replace('#', ''); 144 if (h.length === 3) { 145 h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]; 146 } 147 return { 148 r: parseInt(h.slice(0, 2), 16), 149 g: parseInt(h.slice(2, 4), 16), 150 b: parseInt(h.slice(4, 6), 16) 151 }; 152 } 153 154 function rgbToHex(r, g, b) { 155 function c(v) { 156 var s = Math.max(0, Math.min(255, Math.round(v))).toString(16); 157 return s.length === 1 ? '0' + s : s; 158 } 159 return '#' + c(r) + c(g) + c(b); 160 } 161
162 function resolveColor(part, t, time) { 163 if (part.colorMode === 'solid') { 164 if (part.solidColor.indexOf('hsl') === 0) return part.solidColor; 165 var rgb = hexToRgb(part.solidColor); 166 var shift = Math.sin(t * 6.28 + time * 0.0015) * 12 + t * 8; 167 return rgbToHex(rgb.r + shift * 0.4, rgb.g + shift * 0.55, rgb.b + shift * 0.9); 168 } 169 if (part.colorMode === 'rgb') { 170 var hue = (time * part.rgbSpeed * 0.06 + t * 220) % 360; 171 return 'hsl(' + hue + ', 72%, 58%)'; 172 } 173 var stripes = PRIDE_PRESETS[part.pridePreset] || PRIDE_PRESETS.rainbow; 174 var idx = Math.floor(t * stripes.length) % stripes.length; 175 if (idx < 0) idx = 0; 176 return stripes[idx]; 177 } 178 179 function buildKey() { 180 return [config.size, config.tentacleDensity].join(':'); 181 } 182 183 function depsReady() { 184 return !!(global.THREE && global.Particulate && global.JellypuffMedusae && 185 global.JellypuffMedusaeUtils && global.JellypuffMedusaeMaterials); 186 } 187 188 function setThreeColor(target, cssColor) { 189 if (!target || !cssColor) return; 190 if (cssColor.indexOf('hsl') === 0) target.setStyle(cssColor); 191 else target.set(cssColor); 192 } 193 194 function applyPalette() { 195 if (!medusae) return; 196 var time = medusae.animTime * 1000; 197 var bell = resolveColor(config.bell, 0.2, time); 198 var bellAlt = resolveColor(config.bell, 0.65, time); 199 var tent = resolveColor(config.tentacles, 0.45, time); 200 var gel = resolveColor(config.bell, 0.8, time) || DEFAULT_GEL;
201 if (medusae.bulbMesh) { 202 setThreeColor(medusae.bulbMesh.material.uniforms.diffuse.value, bell); 203 setThreeColor(medusae.bulbMesh.material.uniforms.diffuseB.value, bellAlt); 204 } 205 if (medusae.bulbFaintMesh) { 206 setThreeColor(medusae.bulbFaintMesh.material.uniforms.diffuse.value, gel); 207 } 208 if (medusae.tentacleFore) { 209 setThreeColor(medusae.tentacleFore.material.uniforms.diffuse.value, tent); 210 } 211 if (medusae.tailMesh) { 212 setThreeColor(medusae.tailMesh.material.uniforms.diffuse.value, tent); 213 } 214 medusae.setGlowStrength(glowFactor()); 215 } 216 217 function rebuildMedusae() { 218 if (!depsReady()) return; 219 if (medusae && medusae.item && scene) { 220 scene.remove(medusae.item); 221 } 222 medusae = global.JellypuffMedusae.create({ 223 pxRatio: global.devicePixelRatio || 1, 224 scale: scaleFactor(), 225 density: densityFactor() 226 }); 227 scene.add(medusae.item); 228 lastBuildKey = buildKey(); 229 applyPalette(); 230 } 231 232 function maybeRebuildMedusae() { 233 var key = buildKey(); 234 if (key !== lastBuildKey) rebuildMedusae(); 235 else applyPalette(); 236 } 237 238 function resizeCamera() { 239 var w = global.innerWidth || 1; 240 var h = global.innerHeight || 1; 241 if (!camera || !renderer) return; 242 camera.left = -w * 0.5; 243 camera.right = w * 0.5; 244 camera.top = h * 0.5; 245 camera.bottom = -h * 0.5; 246 camera.updateProjectionMatrix(); 247 renderer.setSize(w, h); 248 renderer.setPixelRatio(Math.min(global.devicePixelRatio || 1, 2)); 249 } 250 251 function screenToWorld(x, y) { 252 var w = global.innerWidth || 1; 253 var h = global.innerHeight || 1; 254 return { 255 x: x - w * 0.5, 256 y: h * 0.5 - y 257 }; 258 } 259 260 function aureliaPulsePhaseRad() { 261 var cycle = animTime * 0.2; 262 cycle = cycle - Math.floor(cycle); 263 return cycle * Math.PI * 2; 264 } 265 266 function pulseWave() { 267 return Math.sin(aureliaPulsePhaseRad() + 4.4); 268 } 269 270 function contractStrength(wave) { 271 var c = Math.max(0, wave); 272 return c * c * (3 - 2 * c); 273 } 274 275 function updatePointerCharge(frameDt) { 276 var dist = Math.hypot(mouse.x - pos.x, mouse.y - pos.y); 277 var near = 1 - Math.min(Math.max(0, dist - 48), 180) / 180; 278 charge += near * 0.022 * frameDt; 279 charge = Math.min(charge, 0.75); 280 charge *= Math.pow(0.96, frameDt); 281 } 282 283 function computeTiltTarget(time) { 284 var wobble = Math.sin(time * WOBBLE_RATE + wobbleSeed) * WOBBLE_AMP; 285 wobble += Math.sin(time * 0.29 + wobbleSeed * 1.63) * WOBBLE_AMP * 0.42; 286 var leanRaw = Math.atan2(mouse.x - pos.x, -(mouse.y - pos.y)) * LEAN_AMP; 287 leanTilt += (-Math.max(-0.11, Math.min(0.11, leanRaw)) - leanTilt) * LEAN_SMOOTH; 288 return wobble + leanTilt; 289 } 290 291 function updateMotion(dt) { 292 var sf = scaleFactor(); 293 var pulseMul = config.pulseRate / 100; 294 var rawDist = Math.hypot(mouse.x - pos.x, mouse.y - pos.y); 295 var dx = mouse.x - pos.x; 296 var dy = mouse.y - pos.y; 297 var wave; 298 var contract; 299 var chargeBoost; 300 var jetImpulse; 301 var sinkRate; 302 var steerImpulse; 303 var tiltTarget; 304 var verticalIntent; 305 var jetScale; 306 var sinkScale; 307 var distUrgency; 308 var chaseImpulse; 309 var maxSpeed; 310 var viewH = global.innerHeight || 600; 311 var topGuard = 72 * sf; 312 var bottomGuard = viewH - 72 * sf; 313 314 smoothDist += (rawDist - smoothDist) * 0.09 * dt; 315 updatePointerCharge(dt); 316 317 distUrgency = 1 + Math.min(smoothDist / 380, 0.62); 318 animTime += dt * TIME_RATE * pulseMul * distUrgency * (1 + charge * 0.35); 319 320 wave = pulseWave(); 321 contract = contractStrength(wave); 322 chargeBoost = 1 + charge * CHARGE_GAIN; 323 bellPhase = contract; 324 smoothBellPhase += (bellPhase - smoothBellPhase) * 0.24 * dt; 325 326 verticalIntent = Math.max(-1, Math.min(1, dy / 220)); 327 jetScale = 1; 328 sinkScale = 1; 329 330 if (dy > 24) { 331 jetScale = Math.max(0.38, 1 - verticalIntent * 0.58); 332 sinkScale = 1 + verticalIntent * (0.62 + Math.min(smoothDist / 520, 0.42)); 333 } else if (dy < -24) { 334 jetScale = 1 + Math.min(0.38, -verticalIntent * (0.22 + Math.min(smoothDist / 520, 0.3))); 335 sinkScale = Math.max(0.76, 1 + verticalIntent * 0.14); 336 } 337 338 if (pos.y < topGuard) { 339 jetScale *= 0.45; 340 sinkScale += 0.55; 341 } else if (pos.y > bottomGuard) { 342 jetScale = Math.min(jetScale * 1.35, 1.55); 343 sinkScale *= 0.62; 344 } 345 346 var midY = viewH * 0.5; 347 var lowDrift = Math.max(0, pos.y - (midY + 48 * sf)); 348 if (lowDrift > 0) { 349 var lowNorm = Math.min(lowDrift / (viewH * 0.32), 1); 350 jetScale += lowNorm * DRIFT_RECOVERY; 351 sinkScale *= 1 - lowNorm * 0.32; 352 } 353 354 jetImpulse = contract * JET_PEAK * chargeBoost * sf * jetScale * dt; 355 sinkRate = BASE_SINK * (0.72 + (1 - contract) * 0.28) * sinkScale; 356 357 pos.y += sinkRate * dt; 358 pos.y -= jetImpulse; 359 360 chaseImpulse = contract * CHASE_GAIN * Math.min(smoothDist / 280, 1.15) * chargeBoost * sf * dt; 361 if (chaseImpulse > 0.001 && rawDist > 1) { 362 pos.x += (dx / rawDist) * chaseImpulse; 363 pos.y += (dy / rawDist) * chaseImpulse * 0.52; 364 } 365 366 vel.x *= Math.pow(HORIZ_DAMP, dt); 367 steerImpulse = (dx / Math.max(rawDist, 1)) * HORIZ_STEER * (0.38 + contract * 1.15) * chargeBoost * sf * distUrgency * dt; 368 vel.x += steerImpulse; 369 370 maxSpeed = MAX_HORIZ_SPEED * (0.72 + Math.min(smoothDist / 520, 0.45)); 371 if (vel.x > maxSpeed) vel.x = maxSpeed; 372 if (vel.x < -maxSpeed) vel.x = -maxSpeed; 373 vel.y = jetImpulse - sinkRate * dt; 374 375 pos.x += vel.x * dt; 376 377 if (pos.y < topGuard * 0.35) { 378 pos.y = topGuard * 0.35; 379 vel.y = Math.max(vel.y, 0); 380 } else if (pos.y > viewH - topGuard * 0.35) { 381 pos.y = viewH - topGuard * 0.35; 382 vel.y = Math.min(vel.y, 0); 383 pos.y -= (2.4 + contract * 4.8) * sf * dt; 384 } 385 386 tiltTarget = computeTiltTarget(animTime); 387 renderTilt += (tiltTarget - renderTilt) * TILT_SMOOTH * dt; 388 389 if (medusae) { 390 medusae.applySwimForces(vel.x, vel.y); 391 medusae.updatePins(smoothBellPhase, smoothBellPhase * PIN_JET_SCALE * sf); 392 } 393 394 if (medusae && medusae.item) { 395 var world = screenToWorld(pos.x, pos.y); 396 var travelLean = vel.x * TRAVEL_LEAN;
397 var aimLean = Math.max(-0.1, Math.min(0.1, dx * 0.0003)); 398 medusae.item.position.set(world.x, world.y, 0); 399 medusae.item.rotation.set( 400 -smoothBellPhase * 0.07, 401 0, 402 -(travelLean + aimLean + renderTilt) 403 ); 404 } 405 } 406 407 function physicsStep(deltaMs) { 408 if (!medusae) return; 409 medusae.update(deltaMs, smoothBellPhase); 410 } 411 412 function renderStep(deltaMs, stepProgress) { 413 if (!renderer || !scene || !camera || !medusae) return; 414 var dt = deltaMs / 16.67; 415 if (dt <= 0 || !Number.isFinite(dt)) dt = 1; 416 dt = Math.min(dt, 1.35); 417 updateMotion(dt); 418 if (config.bell.colorMode !== 'solid' || config.tentacles.colorMode !== 'solid') { 419 applyPalette(); 420 } 421 medusae.updateGraphics(deltaMs, stepProgress); 422 renderer.render(scene, camera); 423 } 424 425 function spawnAtCenter() { 426 pos.x = global.innerWidth * 0.5; 427 pos.y = global.innerHeight * 0.5; 428 mouse.x = pos.x; 429 mouse.y = pos.y; 430 vel.x = 0; 431 vel.y = 0; 432 charge = 0; 433 smoothDist = 0; 434 smoothBellPhase = 0; 435 bellPhase = 0; 436 renderTilt = 0; 437 leanTilt = 0; 438 animTime = Math.random() * 5; 439 } 440 441 function startLoop() { 442 if (looper) looper.stop(); 443 if (!global.JellypuffMedusaeUtils) return; 444 looper = new global.JellypuffMedusaeUtils.Looper( 445 { physicsStep: physicsStep, renderStep: renderStep }, 446 'physicsStep', 447 'renderStep', 448 1000 / 30 449 ); 450 looper.start(); 451 } 452 453 function stopLoop() { 454 if (looper) { 455 looper.stop(); 456 looper = null; 457 } 458 if (animId) { 459 global.cancelAnimationFrame(animId); 460 animId = null; 461 } 462 } 463 464 function onMouseMove(e) { 465 mouse.x = e.clientX; 466 mouse.y = e.clientY; 467 } 468 469 function onResize() { 470 resizeCamera(); 471 } 472 473 function onSettingChanged(e) { 474 if (!e || !e.detail) return; 475 if (e.detail.key === 'jellypuff' || e.detail.key === 'follower') { 476 applySettings(readConfig()); 477 } 478 } 479 480 function applySettings(next) { 481 config = mergeConfig(next); 482 if (!isActive) return; 483 maybeRebuildMedusae(); 484 } 485 486 function initRenderer() { 487 if (!depsReady()) { 488 emitDevWarn('Jellypuff: missing WebGL dependencies (THREE, Particulate, Medusae modules)'); 489 return false; 490 } 491 492 container = document.createElement('div'); 493 container.id = 'jellypuff-follower'; 494 container.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999;isolation:isolate;'; 495 renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, powerPreference: 'high-performance' }); 496 renderer.setClearColor(0x000000, 0); 497 renderer.domElement.setAttribute('aria-hidden', 'true'); 498 renderer.domElement.style.cssText = 'width:100%;height:100%;display:block;'; 499 container.appendChild(renderer.domElement); 500 document.body.appendChild(container); 501 502 scene = new THREE.Scene(); 503 camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 2000); 504 camera.position.z = 500; 505 resizeCamera(); 506 return true; 507 } 508 509 function init() { 510 if (isActive) { 511 spawnAtCenter(); 512 applySettings(readConfig()); 513 onResize(); 514 startLoop(); 515 return; 516 } 517 config = readConfig(); 518 if (!initRenderer()) return; 519 520 spawnAtCenter(); 521 rebuildMedusae(); 522 523 global.addEventListener('mousemove', onMouseMove); 524 global.addEventListener('resize', onResize); 525 document.addEventListener('pupwife:setting-changed', onSettingChanged); 526 527 isActive = true; 528 startLoop(); 529 emitDevLog('Jellypuff follower active (Medusae WebGL)'); 530 } 531 532 function cleanup() { 533 if (!isActive) return; 534 isActive = false;
535 stopLoop(); 536 global.removeEventListener('mousemove', onMouseMove); 537 global.removeEventListener('resize', onResize); 538 document.removeEventListener('pupwife:setting-changed', onSettingChanged); 539 if (medusae && medusae.item && scene) { 540 scene.remove(medusae.item); 541 } 542 medusae = null; 543 if (renderer) { 544 renderer.dispose(); 545 } 546 renderer = null; 547 scene = null; 548 camera = null; 549 if (container && container.parentNode) { 550 container.parentNode.removeChild(container); 551 } 552 container = null; 553 emitDevLog('Jellypuff follower cleaned up'); 554 } 555 556 global.JellypuffFollower = { 557 init: init, 558 cleanup: cleanup, 559 isActive: function () { return isActive; }, 560 applySettings: applySettings, 561 defaultConfig: defaultConfig, 562 mergeConfig: mergeConfig, 563 PRIDE_PRESETS: PRIDE_PRESETS 564 }; 565})(window);
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.