PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:05:25 UTC 17,601 bytes, 478 lines download raw bytes

1/**
2 * Woim On A String — squirmle cursor follower (kinematic rope + fuzzy body).
3 */
4(function (global) {
5    'use strict';
6
7    var BASE_ROPE_SPAN = 118;
8    var BASE_BODY_SPAN = 88;
9    var BASE_ROPE_WIDTH = 1.5;
10    var BASE_ANCHOR_R = 6;
11    var BASE_ANCHOR_STROKE = 1.5;
12    var ANCHOR_LERP = 0.15;
13    var BRISTLE_COUNT = 26;
14    var DEFAULT_SOLID_WOIM = '#ff4da6';
15    var DEFAULT_SOLID_ROPE = '#b8b8c8';
16
17    var PRIDE_PRESETS = {
18        rainbow: ['#e40303', '#ff8c00', '#ffed00', '#008026', '#24408e', '#732982'],
19        trans: ['#5BCEFA', '#F5A9B8', '#FFFFFF', '#F5A9B8', '#5BCEFA'],
20        bi: ['#D60270', '#9B4F96', '#0038A8'],
21        pan: ['#FF218C', '#FFD800', '#21B1FF'],
22        lesbian: ['#D52D00', '#FF9A56', '#FFFFFF', '#D362A4', '#A40062'],
23        nb: ['#FCF434', '#FFFFFF', '#9C59D1', '#2C2C2C'],
24        ace: ['#000000', '#A3A3A3', '#FFFFFF', '#810081'],
25        progress: ['#FFFFFF', '#FFAEC9', '#74D7EE', '#613915', '#000000']
26    };
27
28    var container = null;
29    var canvas = null;
30    var ctx = null;
31    var animId = null;
32    var isActive = false;
33    var points = [];
34    var ropeSimCount = 32;
35    var bodyPointCount = 32;
36    var mouse = { x: 0, y: 0 };
37    var fuzzPhase = 0;
38    var lastFrame = 0;
39    var config = null;
40
41    function emitDevLog() {
42        if (global.devLog) global.devLog.apply(global, arguments);
43    }
44
45    function defaultColorPart(solid) {
46        return { colorMode: 'solid', solidColor: solid, rgbSpeed: 1, pridePreset: 'rainbow' };
47    }
48
49    function defaultConfig() {
50        return {
51            ropePoints: 32,
52            bodyPoints: 32,
53            size: 100,
54            ropeLength: 100,
55            ropeThickness: 100,
56            anchorShow: true,
57            anchorSize: 100,
58            anchorThickness: 100,
59            woim: defaultColorPart(DEFAULT_SOLID_WOIM),
60            rope: defaultColorPart(DEFAULT_SOLID_ROPE)
61        };
62    }
63
64    function mergeConfig(raw) {
65        var d = defaultConfig();
66        if (!raw || typeof raw !== 'object') return d;
67        return {
68            ropePoints: clampInt(raw.ropePoints, 16, 64, d.ropePoints),
69            bodyPoints: clampInt(raw.bodyPoints, 16, 64, d.bodyPoints),
70            size: clampInt(raw.size, 25, 400, d.size),
71            ropeLength: clampInt(raw.ropeLength, 50, 200, d.ropeLength),
72            ropeThickness: clampInt(raw.ropeThickness, 25, 300, d.ropeThickness),
73            anchorShow: typeof raw.anchorShow === 'boolean' ? raw.anchorShow : d.anchorShow,
74            anchorSize: clampInt(raw.anchorSize, 50, 200, d.anchorSize),
75            anchorThickness: clampInt(raw.anchorThickness, 50, 200, d.anchorThickness),
76            woim: mergeColorPart(raw.woim, d.woim),
77            rope: mergeColorPart(raw.rope, d.rope)
78        };
79    }
80
81    function mergeColorPart(raw, fallback) {
82        if (!raw || typeof raw !== 'object') return Object.assign({}, fallback);
83        var mode = raw.colorMode;
84        if (mode !== 'solid' && mode !== 'rgb' && mode !== 'pride') mode = fallback.colorMode;
85        return {
86            colorMode: mode,
87            solidColor: typeof raw.solidColor === 'string' ? raw.solidColor : fallback.solidColor,
88            rgbSpeed: clampNum(raw.rgbSpeed, 0.2, 3, fallback.rgbSpeed),
89            pridePreset: PRIDE_PRESETS[raw.pridePreset] ? raw.pridePreset : fallback.pridePreset
90        };
91    }
92
93    function clampInt(v, min, max, fb) {
94        var n = parseInt(v, 10);
95        if (Number.isNaN(n)) return fb;
96        return Math.max(min, Math.min(max, n));
97    }
98
99    function clampNum(v, min, max, fb) {
100        var n = parseFloat(v);
101        if (Number.isNaN(n)) return fb;
102        return Math.max(min, Math.min(max, n));
103    }
104
105    function readConfig() {
106        if (global.SettingsMenu && typeof global.SettingsMenu.getSetting === 'function') {
107            return mergeConfig(global.SettingsMenu.getSetting('woimString'));
108        }
109        return defaultConfig();
110    }
111
112    function lerp(a, b, t) {
113        return a + (b - a) * t;
114    }
115
116    function ropeSegLen() {
117        return (BASE_ROPE_SPAN * (config.ropeLength / 100)) / Math.max(1, ropeSimCount);
118    }
119
120    function bodySegLen() {
121        var count = Math.max(1, bodyPointCount);
122        return (BASE_BODY_SPAN * (config.size / 100)) / count;
123    }
124
125    function rebuildChain() {
126        bodyPointCount = config.bodyPoints;
127        ropeSimCount = config.ropePoints;
128        var total = 1 + ropeSimCount + bodyPointCount;
129        var sx = mouse.x;
130        var sy = mouse.y;
131        var seg = ropeSegLen();
132        points = [];
133        for (var i = 0; i < total; i++) {
134            points.push({ x: sx, y: sy + i * seg });
135        }
136    }
137
138
139    function resolveColor(part, t, time) {
140        if (part.colorMode === 'solid') {
141            return part.solidColor;
142        }
143        if (part.colorMode === 'rgb') {
144            var hue = (time * part.rgbSpeed * 0.08 + t * 280) % 360;
145            return 'hsl(' + hue + ', 88%, 58%)';
146        }
147        var stripes = PRIDE_PRESETS[part.pridePreset] || PRIDE_PRESETS.rainbow;
148        var idx = Math.floor(t * stripes.length) % stripes.length;
149        if (idx < 0) idx = 0;
150        return stripes[idx];
151    }
152
153    function updateChain() {
154        if (!points.length) return;
155
156        points[0].x = lerp(points[0].x, mouse.x, ANCHOR_LERP);
157        points[0].y = lerp(points[0].y, mouse.y, ANCHOR_LERP);
158
159        for (var i = 1; i < points.length; i++) {
160            var pdx = points[i].x - points[i - 1].x;
161            var pdy = points[i].y - points[i - 1].y;
162            var angle = Math.atan2(pdy, pdx);
163            if (i === 1 && (pdx * pdx + pdy * pdy) < 0.25) {
164                angle = Math.PI / 2;
165            }
166            var seg = i <= ropeSimCount ? ropeSegLen() : bodySegLen();
167            points[i].x = points[i - 1].x + seg * Math.cos(angle);
168            points[i].y = points[i - 1].y + seg * Math.sin(angle);
169        }
170    }
171
172    function bodyRadiusAt(t) {
173        var scale = config.size / 100;
174        var nose = 0.35 + 0.65 * Math.max(0, 1 - t * 2.5);
175        var belly = 0.55 + 0.45 * Math.sin(t * Math.PI);
176        var r = (t < 0.12 ? nose : belly) * 8 * scale;
177        return Math.max(2.5, r);
178    }
179
180    function drawAnchor(time) {
181        if (!config || config.anchorShow !== true) return;
182        var r = BASE_ANCHOR_R * (config.anchorSize / 100);
183        var sw = BASE_ANCHOR_STROKE * (config.anchorThickness / 100);
184        var ringColor = resolveColor(config.rope, 0, time);
185        ctx.strokeStyle = ringColor;
186        ctx.lineWidth = sw;
187        ctx.globalAlpha = 0.75;
188        ctx.beginPath();
189        ctx.arc(points[0].x, points[0].y, r, 0, Math.PI * 2);
190        ctx.stroke();
191        ctx.globalAlpha = 1;
192    }
193
194    function drawRope(time) {
195        var lw = BASE_ROPE_WIDTH * (config.ropeThickness / 100);
196        if (ropeSimCount < 1 || points.length <= ropeSimCount) return;
197
198        ctx.lineCap = 'round';
199        ctx.lineJoin = 'round';
200        ctx.beginPath();
201        ctx.moveTo(points[0].x, points[0].y);
202        for (var i = 1; i <= ropeSimCount; i++) {
203            ctx.lineTo(points[i].x, points[i].y);
204        }
205        var tail = points[ropeSimCount];
206        var grad = ctx.createLinearGradient(
207            points[0].x, points[0].y,
208            tail.x, tail.y
209        );
210        for (var g = 0; g <= 4; g++) {
211            var gt = g / 4;
212            grad.addColorStop(gt, resolveColor(config.rope, gt, time));
213        }
214        ctx.strokeStyle = grad;
215        ctx.lineWidth = lw;
216        ctx.stroke();
217    }
218
219    function drawFuzzAt(mx, my, ang, rad, col, t, segIdx, time) {
220        var core = ctx.createRadialGradient(mx, my, 0, mx, my, rad * 1.15);
221        core.addColorStop(0, col);
222        core.addColorStop(0.55, col);
223        core.addColorStop(1, 'rgba(0,0,0,0)');
224        ctx.fillStyle = core;
225        ctx.globalAlpha = 0.42;
226        ctx.beginPath();
227        ctx.ellipse(mx, my, rad * 1.15, rad * 0.9, ang, 0, Math.PI * 2);
228        ctx.fill();
229        ctx.globalAlpha = 1;
230
231        for (var pass = 0; pass < 3; pass++) {
232            var count = pass === 0 ? BRISTLE_COUNT : (pass === 1 ? Math.floor(BRISTLE_COUNT * 0.7) : Math.floor(BRISTLE_COUNT * 0.45));
233            var lenMul = pass === 0 ? 1.05 : (pass === 1 ? 0.72 : 0.48);
234            var widthMul = pass === 0 ? 1 : (pass === 1 ? 0.75 : 0.55);
235            var alpha = pass === 0 ? 0.9 : (pass === 1 ? 0.62 : 0.4);
236            for (var bj = 0; bj < count; bj++) {
237                var ba = ang + Math.PI / 2 + (bj / count - 0.5) * Math.PI * 0.98;
238                var wobble = Math.sin(fuzzPhase + segIdx * 0.22 + bj * 0.41 + pass) * 0.12;
239                var blen = rad * (0.9 + wobble) * lenMul;
240                ctx.strokeStyle = col;
241                ctx.lineWidth = (1.2 - pass * 0.22) * widthMul;
242                ctx.globalAlpha = alpha;
243                ctx.beginPath();
244                ctx.moveTo(mx + Math.cos(ba) * rad * 0.15, my + Math.sin(ba) * rad * 0.15);
245                ctx.lineTo(mx + Math.cos(ba + wobble * 0.3) * blen, my + Math.sin(ba + wobble * 0.3) * blen);
246                ctx.stroke();
247            }
248        }
249        ctx.globalAlpha = 1;
250    }
251
252    function drawFuzzyBody(time) {
253        var headIdx = ropeSimCount + 1;
254        var end = points.length - 1;
255
256        for (var i = headIdx; i < end; i++) {
257            var t = (i - headIdx) / Math.max(1, bodyPointCount - 1);
258            var a = points[i];
259            var b = points[i + 1];
260            var mx = (a.x + b.x) * 0.5;
261            var my = (a.y + b.y) * 0.5;
262            var ang = Math.atan2(b.y - a.y, b.x - a.x);
263            var rad = bodyRadiusAt(t);
264            var col = resolveColor(config.woim, t, time);
265            drawFuzzAt(mx, my, ang, rad, col, t, i, time);
266        }
267
268        if (points.length > headIdx && points.length > headIdx - 1) {
269            var head = points[headIdx];
270            var towardRope = Math.atan2(
271                head.y - points[headIdx - 1].y,
272                head.x - points[headIdx - 1].x
273            );
274            var headCol = resolveColor(config.woim, 0, time);
275            var headRad = bodyRadiusAt(0);
276            drawNose(head.x, head.y, towardRope + Math.PI, headRad, headCol);
277            drawEyes(headIdx, towardRope);
278        }
279    }
280
281    function drawNose(x, y, ang, rad, col) {
282        var len = rad * 2.5;
283        var baseHalf = rad * 0.78;
284        var perp = ang + Math.PI / 2;
285        var bx = x - Math.cos(ang) * rad * 0.2;
286        var by = y - Math.sin(ang) * rad * 0.2;
287        var tipX = bx + Math.cos(ang) * len;
288        var tipY = by + Math.sin(ang) * len;
289        var leftX = bx + Math.cos(perp) * baseHalf;
290        var leftY = by + Math.sin(perp) * baseHalf;
291        var rightX = bx - Math.cos(perp) * baseHalf;
292        var rightY = by - Math.sin(perp) * baseHalf;
293        var midX = bx + Math.cos(ang) * len * 0.45;
294        var midY = by + Math.sin(ang) * len * 0.45;
295
296        var core = ctx.createRadialGradient(midX, midY, 0, midX, midY, len * 0.95);
297        core.addColorStop(0, col);
298        core.addColorStop(0.6, col);
299        core.addColorStop(1, 'rgba(0,0,0,0)');
300        ctx.fillStyle = core;
301        ctx.globalAlpha = 0.48;
302        ctx.beginPath();
303        ctx.moveTo(leftX, leftY);
304        ctx.lineTo(tipX, tipY);
305        ctx.lineTo(rightX, rightY);
306        ctx.closePath();
307        ctx.fill();
308        ctx.globalAlpha = 1;
309
310        var edgePairs = [
311            { ax: leftX, ay: leftY, bx: tipX, by: tipY },
312            { ax: rightX, ay: rightY, bx: tipX, by: tipY },
313            { ax: leftX, ay: leftY, bx: rightX, by: rightY }
314        ];
315        var bristleCount = Math.max(12, Math.floor(BRISTLE_COUNT * 0.5));
316        var bj;
317        for (bj = 0; bj < bristleCount; bj++) {
318            var t = bj / Math.max(1, bristleCount - 1);
319            var edge = edgePairs[bj % 3];
320            var px = lerp(edge.ax, edge.bx, t);
321            var py = lerp(edge.ay, edge.by, t);
322            var outAng = Math.atan2(py - midY, px - midX);
323            var wobble = Math.sin(fuzzPhase * 1.1 + bj * 0.47) * 0.14;
324            var blen = rad * (0.42 + (1 - t) * 0.55);
325            ctx.strokeStyle = col;
326            ctx.lineWidth = 1.15 - t * 0.35;
327            ctx.globalAlpha = 0.82 * (1 - t * 0.4);
328            ctx.beginPath();
329            ctx.moveTo(px, py);
330            ctx.lineTo(
331                px + Math.cos(outAng + wobble) * blen,
332                py + Math.sin(outAng + wobble) * blen
333            );
334            ctx.stroke();
335        }
336
337        ctx.strokeStyle = col;
338        ctx.lineWidth = 1.1;
339        ctx.globalAlpha = 0.55;
340        ctx.beginPath();
341        ctx.moveTo(leftX, leftY);
342        ctx.lineTo(tipX, tipY);
343        ctx.lineTo(rightX, rightY);
344        ctx.closePath();
345        ctx.stroke();
346        ctx.globalAlpha = 1;
347    }
348
349    function drawEyes(headIdx, faceAng) {
350        if (points.length <= headIdx + 1) return;
351        var head = points[headIdx];
352        var bodyAng = Math.atan2(
353            points[headIdx + 1].y - head.y,
354            points[headIdx + 1].x - head.x
355        );
356        var scale = config.size / 100;
357        var spacing = 4.5 * scale;
358        var eyeR = 3.2 * scale;
359        var pupilR = 1.2 * scale;
360        var e1x = head.x + Math.cos(bodyAng + Math.PI / 2) * spacing;
361        var e1y = head.y + Math.sin(bodyAng + Math.PI / 2) * spacing;
362        var e2x = head.x + Math.cos(bodyAng - Math.PI / 2) * spacing;
363        var e2y = head.y + Math.sin(bodyAng - Math.PI / 2) * spacing;
364
365        ctx.fillStyle = 'rgba(255,255,255,0.95)';
366        ctx.beginPath();
367        ctx.arc(e1x, e1y, eyeR, 0, Math.PI * 2);
368        ctx.arc(e2x, e2y, eyeR, 0, Math.PI * 2);
369        ctx.fill();
370        ctx.fillStyle = '#1a1a1a';
371        ctx.beginPath();
372        ctx.arc(e1x + Math.cos(faceAng) * 0.8, e1y + Math.sin(faceAng) * 0.8, pupilR, 0, Math.PI * 2);
373        ctx.arc(e2x + Math.cos(faceAng) * 0.8, e2y + Math.sin(faceAng) * 0.8, pupilR, 0, Math.PI * 2);
374        ctx.fill();
375    }
376
377    function tick(now) {
378        if (!isActive || !ctx) return;
379        var dt = Math.min((now - lastFrame) / 16.67, 2.5);
380        lastFrame = now;
381        fuzzPhase += dt * 0.09;
382
383        updateChain();
384
385        ctx.clearRect(0, 0, canvas.width, canvas.height);
386        drawAnchor(now);
387        drawRope(now);
388        drawFuzzyBody(now);
389
390        animId = global.requestAnimationFrame(tick);
391    }
392
393    function onMouseMove(e) {
394        mouse.x = e.clientX;
395        mouse.y = e.clientY;
396    }
397
398    function onResize() {
399        if (!canvas) return;
400        canvas.width = global.innerWidth;
401        canvas.height = global.innerHeight;
402    }
403
404    function onSettingChanged(e) {
405        if (!e || !e.detail) return;
406        if (e.detail.key === 'woimString' || e.detail.key === 'follower') {
407            applySettings(readConfig());
408        }
409    }
410
411    function applySettings(next) {
412        var prevBody = config ? config.bodyPoints : 0;
413        var prevRope = config ? config.ropePoints : 0;
414        config = mergeConfig(next);
415        if (!isActive) return;
416        if (config.bodyPoints !== prevBody || config.ropePoints !== prevRope) {
417            rebuildChain();
418        }
419    }
420
421    function init() {
422        if (isActive) return;
423        config = readConfig();
424
425        container = document.createElement('div');
426        container.id = 'woim-string-follower';
427        container.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:998;';
428        canvas = document.createElement('canvas');
429        canvas.style.cssText = 'width:100%;height:100%;display:block;';
430        container.appendChild(canvas);
431        document.body.appendChild(container);
432        ctx = canvas.getContext('2d');
433        onResize();
434
435        mouse.x = global.innerWidth / 2;
436        mouse.y = global.innerHeight / 2;
437        rebuildChain();
438
439        global.addEventListener('mousemove', onMouseMove);
440        global.addEventListener('resize', onResize);
441        document.addEventListener('pupwife:setting-changed', onSettingChanged);
442
443        isActive = true;
444        lastFrame = performance.now();
445        animId = global.requestAnimationFrame(tick);
446        emitDevLog('Woim On A String follower active');
447    }
448
449    function cleanup() {
450        if (!isActive) return;
451        isActive = false;
452        if (animId) {
453            global.cancelAnimationFrame(animId);
454            animId = null;
455        }
456        global.removeEventListener('mousemove', onMouseMove);
457        global.removeEventListener('resize', onResize);
458        document.removeEventListener('pupwife:setting-changed', onSettingChanged);
459        if (container && container.parentNode) {
460            container.parentNode.removeChild(container);
461        }
462        container = null;
463        canvas = null;
464        ctx = null;
465        points = [];
466        emitDevLog('Woim On A String follower cleaned up');
467    }
468
469    global.WoimStringFollower = {
470        init: init,
471        cleanup: cleanup,
472        isActive: function () { return isActive; },
473        applySettings: applySettings,
474        defaultConfig: defaultConfig,
475        mergeConfig: mergeConfig,
476        PRIDE_PRESETS: PRIDE_PRESETS
477    };
478})(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.