PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:05:30 UTC 18,655 bytes, 565 lines download raw bytes

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.