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.