1/** 2 * Stretchy paw cursor â cat or dog paw on a configurable arm from the viewport edge. 3 */ 4(function (global) { 5 'use strict'; 6 7 var BASE_ARM_WIDTH = 30; 8 var BASE_PAW_SCALE = 1; 9 var BASE_OUTLINE = 1.4; 10 var PRINT_BASE_MS = 3000; 11 var SPRITE_TARGET_HEIGHT = 36; 12 var SPRITE_ARM_COLORS = { 13 fill: '#ffffff', 14 edge: '#bdbdbd', 15 fleck: '#e8e8e8' 16 }; 17 var PAW_PRESETS = { 18 'classic-white': { 19 id: 'classic-white', 20 label: 'Classic white', 21 species: 'cat', 22 spriteBase: '/cursors/Paws/Paws/catpaw_base.png', 23 spriteBeans: '/cursors/Paws/Paws/catpaw_beans.png', 24 meta: { toesY: 0.05, wristY: 0.99, wristWidth: 0.715 }, 25 recolorFur: true, 26 defaultFur: '#ffffff', 27 armEdge: '#bdbdbd', 28 armFleck: '#e8e8e8' 29 } 30 }; 31 32 var PAW_SPRITE_PATHS = {}; 33 var PAW_SPRITE_META = {}; 34 Object.keys(PAW_PRESETS).forEach(function (id) { 35 var preset = PAW_PRESETS[id]; 36 PAW_SPRITE_PATHS[id] = preset.spriteBase || preset.sprite; 37 PAW_SPRITE_META[id] = preset.meta; 38 }); 39 40 var PRIDE_PRESETS = { 41 rainbow: ['#e40303', '#ff8c00', '#ffed00', '#008026', '#24408e', '#732982'], 42 trans: ['#5BCEFA', '#F5A9B8', '#FFFFFF', '#F5A9B8', '#5BCEFA'], 43 bi: ['#D60270', '#9B4F96', '#0038A8'], 44 pan: ['#FF218C', '#FFD800', '#21B1FF'], 45 lesbian: ['#D52D00', '#FF9A56', '#FFFFFF', '#D362A4', '#A40062'], 46 nb: ['#FCF434', '#FFFFFF', '#9C59D1', '#2C2C2C'], 47 ace: ['#000000', '#A3A3A3', '#FFFFFF', '#810081'], 48 progress: ['#FFFFFF', '#FFAEC9', '#74D7EE', '#613915', '#000000'] 49 }; 50 51 var ANCHOR_PRESETS = { 52 'bottom-center': { originX: 50, originY: 108 }, 53 'bottom-left': { originX: 8, originY: 108 }, 54 'bottom-right': { originX: 92, originY: 108 }, 55 'top-center': { originX: 50, originY: -8 }, 56 'top-left': { originX: 8, originY: -8 }, 57 'top-right': { originX: 92, originY: -8 }, 58 'middle-left': { originX: 0, originY: 50 }, 59 'middle-right': { originX: 100, originY: 50 }, 60 custom: null 61 }; 62 63 function defaultColorPart(hex) { 64 return { 65 colorMode: 'solid', 66 solidColor: hex, 67 rgbSpeed: 1, 68 pridePreset: 'rainbow' 69 }; 70 } 71 72 function defaultConfig() { 73 return { 74 anchorPreset: 'bottom-center', 75 originX: 50, 76 originY: 108, 77 armThickness: 100, 78 armTaper: 35, 79 armBend: 0, 80 armMargin: 100, 81 followLag: 42, 82 clickSquish: true, 83 showBeans: true, 84 pawPreset: 'classic-white', 85 pawSpecies: 'cat', 86 pawSize: 100, 87 padSize: 100, 88 printsEnabled: true, 89 printSize: 100, 90 printFade: 100, 91 printMax: 16, 92 outlineShow: true, 93 outlineThickness: 100, 94 arm: defaultColorPart('#ffffff'), 95 pawFur: defaultColorPart('#ffffff'), 96 pawPads: defaultColorPart('#f2a7b8'), 97 outline: defaultColorPart('#b0b0b0'), 98 prints: defaultColorPart('#e89ab0') 99 }; 100 } 101 102 function clampInt(v, min, max, fb) { 103 var n = parseInt(v, 10); 104 if (Number.isNaN(n)) return fb; 105 return Math.max(min, Math.min(max, n)); 106 } 107 108 function clampNum(v, min, max, fb) { 109 var n = parseFloat(v); 110 if (Number.isNaN(n)) return fb; 111 return Math.max(min, Math.min(max, n)); 112 } 113 114 function mergeColorPart(raw, fallback) { 115 if (!raw || typeof raw !== 'object') return Object.assign({}, fallback); 116 var mode = raw.colorMode; 117 if (mode !== 'solid' && mode !== 'rgb' && mode !== 'pride') mode = fallback.colorMode; 118 return { 119 colorMode: mode, 120 solidColor: typeof raw.solidColor === 'string' ? raw.solidColor : fallback.solidColor, 121 rgbSpeed: clampNum(raw.rgbSpeed, 0.2, 3, fallback.rgbSpeed), 122 pridePreset: PRIDE_PRESETS[raw.pridePreset] ? raw.pridePreset : fallback.pridePreset 123 }; 124 } 125 126 function resolvePawPresetId(raw, species) { 127 if (species === 'dog') return ''; 128 if (raw && raw.pawPreset && PAW_PRESETS[raw.pawPreset]) return raw.pawPreset; 129 return 'classic-white'; 130 } 131 132 function mergeConfig(raw) { 133 var d = defaultConfig(); 134 if (!raw || typeof raw !== 'object') return d; 135 var species = raw.pawSpecies === 'dog' ? 'dog' : 'cat'; 136 var pawPreset = resolvePawPresetId(raw, species); 137 var presetDef = getPawPreset(pawPreset);
138 if (presetDef) species = presetDef.species; 139 var preset = ANCHOR_PRESETS[raw.anchorPreset] ? raw.anchorPreset : d.anchorPreset; 140 if (preset === 'center') preset = 'bottom-center'; 141 if (preset === 'custom') preset = 'custom'; 142 var pawSize = clampInt(raw.pawSize, 25, 400, d.pawSize); 143 return { 144 anchorPreset: preset, 145 originX: clampNum(raw.originX, -20, 120, d.originX), 146 originY: clampNum(raw.originY, -20, 120, d.originY), 147 armThickness: pawSize, 148 armTaper: clampInt(raw.armTaper, 0, 100, d.armTaper), 149 armBend: clampInt(raw.armBend, 0, 200, d.armBend), 150 armMargin: 100, 151 followLag: clampInt(raw.followLag, 5, 100, d.followLag), 152 clickSquish: typeof raw.clickSquish === 'boolean' ? raw.clickSquish : d.clickSquish, 153 showBeans: typeof raw.showBeans === 'boolean' ? raw.showBeans : d.showBeans, 154 pawPreset: pawPreset, 155 pawSpecies: species, 156 pawSize: pawSize, 157 padSize: clampInt(raw.padSize, 50, 200, d.padSize), 158 printsEnabled: typeof raw.printsEnabled === 'boolean' ? raw.printsEnabled : d.printsEnabled, 159 printSize: clampInt(raw.printSize, 25, 200, d.printSize), 160 printFade: clampInt(raw.printFade, 25, 200, d.printFade), 161 printMax: clampInt(raw.printMax, 4, 32, d.printMax), 162 outlineShow: typeof raw.outlineShow === 'boolean' ? raw.outlineShow : d.outlineShow, 163 outlineThickness: clampInt(raw.outlineThickness, 50, 200, d.outlineThickness), 164 arm: mergeColorPart(raw.arm, d.arm), 165 pawFur: mergeColorPart(raw.pawFur, d.pawFur), 166 pawPads: mergeColorPart(raw.pawPads, d.pawPads), 167 outline: mergeColorPart(raw.outline, d.outline), 168 prints: mergeColorPart(raw.prints, d.prints) 169 }; 170 } 171 172 var overlay = null; 173 var canvas = null; 174 var printLayer = null; 175 var ctx = null; 176 var animId = null; 177 var isActive = false; 178 var config = null; 179 var target = { x: 0, y: 0 }; 180 var display = { x: 0, y: 0 }; 181 var squish = 1; 182 var squishTarget = 1; 183 var prints = []; 184 var pawSprites = {}; 185 var pawSpriteBeans = {}; 186 var pawSpriteReady = {}; 187 var pawSpriteLoading = {}; 188 var pawSpriteMeasured = {}; 189 var tintedSpriteCache = {}; 190 var measureCanvas = null; 191 var measureCtx = null; 192 var tintCanvas = null; 193 var tintCtx = null; 194 195 function getPawPreset(presetId) { 196 return PAW_PRESETS[presetId] || null; 197 } 198 199 function getActivePresetId() { 200 if (!config || !config.pawPreset || !PAW_PRESETS[config.pawPreset]) return ''; 201 return config.pawPreset; 202 } 203 204 function getActivePreset() { 205 var id = getActivePresetId(); 206 if (!id) return null; 207 return getPawPreset(id); 208 } 209 210 function getPresetOptions() { 211 return Object.keys(PAW_PRESETS).map(function (id) { 212 return { value: id, label: PAW_PRESETS[id].label }; 213 }); 214 } 215 216 function clampByte(v) { 217 return Math.max(0, Math.min(255, Math.round(v))); 218 } 219 220 function parseHexColor(hex) { 221 if (!hex || typeof hex !== 'string') return { r: 255, g: 255, b: 255 }; 222 var h = hex.replace('#', ''); 223 if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]; 224 if (h.length !== 6) return { r: 255, g: 255, b: 255 }; 225 return { 226 r: parseInt(h.slice(0, 2), 16), 227 g: parseInt(h.slice(2, 4), 16), 228 b: parseInt(h.slice(4, 6), 16) 229 }; 230 } 231 232 function getPresetBasePath(preset) { 233 if (!preset) return null; 234 return preset.spriteBase || preset.sprite || null; 235 } 236 237 function getPresetBeansPath(preset) { 238 if (!preset) return null; 239 return preset.spriteBeans || null; 240 } 241 242 function tintFurPixel(r, g, b, a, tr, tg, tb) { 243 var lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; 244 if (lum < 0.01) return [r, g, b, a]; 245 return [clampByte(tr * lum), clampByte(tg * lum), clampByte(tb * lum), a]; 246 } 247 248 function shadeHexColor(hex, factor) { 249 var c = parseHexColor(hex); 250 return ( 251 '#' + 252 clampByte(c.r * factor).toString(16).padStart(2, '0') + 253 clampByte(c.g * factor).toString(16).padStart(2, '0') + 254 clampByte(c.b * factor).toString(16).padStart(2, '0') 255 ); 256 } 257 258 function getSpriteArmColors(now) {
259 var fill = resolveColor(config.pawFur, 0.1, now); 260 return { 261 fill: fill, 262 edge: shadeHexColor(fill, 0.72), 263 fleck: shadeHexColor(fill, 1.08) 264 }; 265 } 266 267 function getTintCacheKey(presetId, furPart, now) { 268 if (!furPart) return presetId + ':default'; 269 if (furPart.colorMode === 'solid') return presetId + ':solid:' + furPart.solidColor; 270 if (furPart.colorMode === 'rgb') return presetId + ':rgb:' + Math.floor(now / 80); 271 return presetId + ':pride:' + furPart.pridePreset + ':' + Math.floor(now / 120); 272 } 273 274 function clearTintedSpriteCache() { 275 tintedSpriteCache = {}; 276 } 277 278 function getTintCtx(width, height) { 279 if (!tintCanvas) { 280 tintCanvas = document.createElement('canvas'); 281 tintCtx = tintCanvas.getContext('2d', { willReadFrequently: true }); 282 } 283 if (tintCanvas.width !== width) tintCanvas.width = width; 284 if (tintCanvas.height !== height) tintCanvas.height = height; 285 return tintCtx; 286 } 287 288 function buildTintedSprite(presetId, furColor) { 289 var img = pawSprites[presetId]; 290 if (!img || !img.naturalWidth) return img; 291 var width = img.naturalWidth; 292 var height = img.naturalHeight; 293 var ctx2d = getTintCtx(width, height); 294 ctx2d.clearRect(0, 0, width, height); 295 ctx2d.drawImage(img, 0, 0); 296 var data = ctx2d.getImageData(0, 0, width, height); 297 var px = data.data; 298 var tr = furColor.r; 299 var tg = furColor.g; 300 var tb = furColor.b; 301 var i; 302 var r; 303 var g; 304 var b; 305 var a; 306 var out; 307 for (i = 0; i < px.length; i += 4) { 308 r = px[i]; 309 g = px[i + 1]; 310 b = px[i + 2]; 311 a = px[i + 3]; 312 if (a < 16) continue; 313 out = tintFurPixel(r, g, b, a, tr, tg, tb); 314 px[i] = out[0]; 315 px[i + 1] = out[1]; 316 px[i + 2] = out[2]; 317 } 318 ctx2d.putImageData(data, 0, 0); 319 return tintCanvas; 320 } 321 322 function getTintedSprite(now) { 323 var preset = getActivePreset(); 324 var presetId = getActivePresetId(); 325 if (!preset || !preset.recolorFur) return pawSprites[presetId] || null; 326 var furHex = resolveColor(config.pawFur, 0.1, now); 327 var cacheKey = getTintCacheKey(presetId, config.pawFur, now); 328 if (tintedSpriteCache[cacheKey]) return tintedSpriteCache[cacheKey]; 329 var tinted = buildTintedSprite(presetId, parseHexColor(furHex)); 330 tintedSpriteCache[cacheKey] = tinted; 331 return tinted; 332 } 333 334 function snapPixel(v) { 335 return Math.round(v); 336 } 337 338 function getMeasureCtx(width, height) { 339 if (!measureCanvas) { 340 measureCanvas = document.createElement('canvas'); 341 measureCtx = measureCanvas.getContext('2d', { willReadFrequently: true }); 342 } 343 if (measureCanvas.width !== width) measureCanvas.width = width; 344 if (measureCanvas.height !== height) measureCanvas.height = height; 345 return measureCtx; 346 } 347 348 function measureRowOpaqueWidth(rowData, width, alphaMin) { 349 var minX = width; 350 var maxX = -1; 351 var x; 352 var threshold = alphaMin || 16; 353 for (x = 0; x < width; x++) { 354 if (rowData[x * 4 + 3] >= threshold) { 355 if (x < minX) minX = x; 356 if (x > maxX) maxX = x; 357 } 358 } 359 return maxX >= minX ? maxX - minX + 1 : 0; 360 } 361 362 function measureSpriteArmWidth(img, meta) { 363 var width = img.naturalWidth; 364 var height = img.naturalHeight; 365 if (!width || !height) return meta.wristWidth; 366 var ctx2d = getMeasureCtx(width, height); 367 ctx2d.clearRect(0, 0, width, height); 368 ctx2d.drawImage(img, 0, 0); 369 var yStart = Math.max(0, Math.floor(height * meta.wristY) - 2); 370 var widths = []; 371 var y; 372 for (y = yStart; y < height; y++) { 373 var rowData = ctx2d.getImageData(0, y, width, 1).data; 374 var rowW = measureRowOpaqueWidth(rowData, width, 200); 375 if (rowW) widths.push(rowW); 376 } 377 if (!widths.length) return meta.wristWidth; 378 widths.sort(function (a, b) { return a - b; }); 379 var medianW = widths[Math.floor(widths.length * 0.5)]; 380 return medianW / width; 381 } 382 383 function measureSpriteMeta(img, presetId) { 384 var base = PAW_PRESETS[presetId] ? PAW_PRESETS[presetId].meta : { toesY: 0.05, wristY: 0.99, wristWidth: 0.715 }; 385 var measured = measureSpriteArmWidth(img, base); 386 var wristWidth = Math.abs(measured - base.wristWidth) < 0.04 387 ? measured 388 : base.wristWidth; 389 pawSpriteMeasured[presetId] = { 390 toesY: base.toesY, 391 wristY: base.wristY, 392 wristWidth: wristWidth 393 }; 394 emitDevLog('Paw sprite wrist width measured:', presetId, Math.round(wristWidth * img.naturalWi
394dth) + 'px', '(' + (wristWidth * 100).toFixed(1) + '% of sprite)'); 395 } 396 397 function getPawSpritePath(presetOrSpecies) { 398 if (PAW_PRESETS[presetOrSpecies]) return getPresetBasePath(PAW_PRESETS[presetOrSpecies]); 399 return PAW_SPRITE_PATHS[presetOrSpecies] || null; 400 } 401 402 function getPawSpriteBeansPath(presetOrSpecies) { 403 if (PAW_PRESETS[presetOrSpecies]) return getPresetBeansPath(PAW_PRESETS[presetOrSpecies]); 404 return null; 405 } 406 407 function isPresetLayersReady(presetId) { 408 if (!pawSpriteReady[presetId]) return false; 409 var beansPath = getPawSpriteBeansPath(presetId); 410 if (!beansPath) return true; 411 var beans = pawSpriteBeans[presetId]; 412 return !!(beans && beans.naturalWidth && beans.naturalHeight); 413 } 414 415 function getPawSpriteMeta(presetOrSpecies) { 416 var measured = pawSpriteMeasured[presetOrSpecies]; 417 if (measured) return measured; 418 if (PAW_PRESETS[presetOrSpecies]) return PAW_PRESETS[presetOrSpecies].meta; 419 return PAW_SPRITE_META[presetOrSpecies] || { toesY: 0.05, wristY: 0.99, wristWidth: 0.715 }; 420 } 421 422 function hasSpriteArt(presetOrSpecies) { 423 return !!getPawSpritePath(presetOrSpecies || (config && config.pawPreset) || 'classic-white'); 424 } 425 426 function hasPresetArt(presetId) { 427 return !!getPawPreset(presetId || (config && config.pawPreset)); 428 } 429 430 function shouldUsePawSprite() { 431 if (!config || config.pawSpecies === 'dog') return false; 432 var presetId = getActivePresetId(); 433 if (!presetId) return false; 434 var path = getPawSpritePath(presetId); 435 if (!path) return false; 436 return isPresetLayersReady(presetId); 437 } 438 439 function preloadPawSprite(presetId) { 440 var preset = PAW_PRESETS[presetId]; 441 if (!preset) return; 442 var basePath = getPresetBasePath(preset); 443 var beansPath = getPresetBeansPath(preset); 444 if (!basePath) return; 445 if (pawSprites[presetId] && (!beansPath || pawSpriteBeans[presetId])) return; 446 if (pawSpriteLoading[presetId]) return; 447 448 pawSpriteLoading[presetId] = true; 449 var pending = beansPath ? 2 : 1; 450 var finished = 0; 451 var baseOk = false; 452 var beansOk = false; 453 454 function onLayerDone() { 455 finished++; 456 if (finished < pending) return; 457 pawSpriteLoading[presetId] = false; 458 pawSpriteReady[presetId] = baseOk && (!beansPath || beansOk); 459 if (pawSpriteReady[presetId]) { 460 try { 461 measureSpriteMeta(pawSprites[presetId], presetId); 462 } catch (e) { 463 emitDevLog('Paw sprite measure failed:', presetId, e); 464 } 465 } else { 466 pawSpriteReady[presetId] = false; 467 } 468 clearTintedSpriteCache(); 469 } 470 471 function loadLayer(path, assign) { 472 var img = new Image(); 473 img.decoding = 'async'; 474 img.onload = function () { 475 if (img.naturalWidth && img.naturalHeight) { 476 assign(img); 477 return onLayerDone(); 478 } 479 onLayerDone(); 480 }; 481 img.onerror = onLayerDone; 482 img.src = path; 483 return img; 484 } 485 486 loadLayer(basePath, function (img) { 487 pawSprites[presetId] = img; 488 baseOk = true; 489 }); 490 if (beansPath) { 491 loadLayer(beansPath, function (img) { 492 pawSpriteBeans[presetId] = img; 493 beansOk = true; 494 }); 495 } 496 } 497 498 function preloadPawSprites() { 499 Object.keys(PAW_PRESETS).forEach(function (id) { 500 preloadPawSprite(id); 501 }); 502 } 503 504 function getSpriteScale() { 505 var img = pawSprites[getActivePresetId()]; 506 if (!img || !img.naturalHeight) return 0; 507 var targetH = SPRITE_TARGET_HEIGHT * (config.pawSize / 100); 508 return targetH / img.naturalHeight; 509 } 510 511 function getSpriteDimensions() { 512 var img = pawSprites[getActivePresetId()]; 513 var scale = getSpriteScale(); 514 if (!img || !scale) return null; 515 return {
516 width: img.naturalWidth * scale, 517 height: img.naturalHeight * scale 518 }; 519 } 520 521 function emitDevLog() { 522 if (global.devLog) global.devLog.apply(global, arguments); 523 } 524 525 function prefersReducedMotion() { 526 try { 527 return !!(global.matchMedia && global.matchMedia('(prefers-reduced-motion: reduce)').matches); 528 } catch (e) { 529 return false; 530 } 531 } 532 533 function isCoarsePointer() { 534 try { 535 return !!(global.matchMedia && global.matchMedia('(pointer: coarse)').matches); 536 } catch (e) { 537 return false; 538 } 539 } 540 541 function isPerformanceMode() { 542 if (global.PERFORMANCE_MODE_ENABLED) return true; 543 if (global.SettingsMenu && typeof global.SettingsMenu.getSetting === 'function') { 544 return !!global.SettingsMenu.getSetting('performanceMode'); 545 } 546 return false; 547 } 548 549 function readConfig() { 550 if (global.SettingsMenu && typeof global.SettingsMenu.getSetting === 'function') { 551 return mergeConfig(global.SettingsMenu.getSetting('catPawArm')); 552 } 553 return defaultConfig(); 554 } 555 556 function resolveColor(part, t, time) { 557 if (!part) return '#ffffff'; 558 if (part.colorMode === 'solid') return part.solidColor; 559 if (part.colorMode === 'rgb') { 560 var hue = (time * part.rgbSpeed * 0.08 + t * 280) % 360; 561 return 'hsl(' + hue + ', 78%, 58%)'; 562 } 563 var stripes = PRIDE_PRESETS[part.pridePreset] || PRIDE_PRESETS.rainbow; 564 var idx = Math.floor(t * stripes.length) % stripes.length; 565 if (idx < 0) idx = 0; 566 return stripes[idx]; 567 } 568 569 function lerp(a, b, t) { 570 return a + (b - a) * t; 571 } 572 573 function getAnchor() { 574 var margin = (config.armMargin / 100) * 24; 575 var ox = (config.originX / 100) * global.innerWidth; 576 var oy = (config.originY / 100) * global.innerHeight; 577 578 if (config.originX < 0) ox = -margin; 579 else if (config.originX > 100) ox = global.innerWidth + margin; 580 else if (config.originX <= 5) ox -= margin * 0.35; 581 else if (config.originX >= 95) ox += margin * 0.35; 582 583 if (config.originY < 0) oy = -margin; 584 else if (config.originY > 100) oy = global.innerHeight + margin; 585 else if (config.originY <= 5) oy -= margin * 0.35; 586 else if (config.originY >= 95) oy += margin * 0.35; 587 588 return { x: ox, y: oy }; 589 } 590 591 function getPawMetrics() { 592 var scale = BASE_PAW_SCALE * (config.pawSize / 100); 593 var padScale = config.padSize / 100; 594 if (shouldUsePawSprite()) { 595 var dims = getSpriteDimensions(); 596 var meta = getPawSpriteMeta(getActivePresetId()); 597 if (dims) { 598 var wristReach = dims.height * (meta.wristY - meta.toesY); 599 return { 600 scale: scale, 601 padScale: padScale, 602 spriteScale: getSpriteScale(), 603 spriteMeta: meta, 604 useSprite: true, 605 hotspotOffset: 0, 606 cuffReach: wristReach, 607 cuffWidth: dims.width * meta.wristWidth, 608 forearmOverlap: Math.max(4, wristReach * 0.12) 609 }; 610 } 611 } 612 return { 613 scale: scale, 614 padScale: padScale, 615 useSprite: false, 616 hotspotOffset: 11 * scale, 617 cuffReach: 23 * scale, 618 cuffWidth: 20 * scale, 619 forearmOverlap: 12 * scale 620 }; 621 } 622 623 function getWristTangent(ax, ay, endX, endY, sideX, sideY) { 624 var cp = getControlPoint(ax, ay, endX, endY, sideX, sideY); 625 return Math.atan2(endY - cp.y, endX - cp.x); 626 } 627 628 function getPawLayout(anchor, hotspot, metrics) { 629 var chordAim = Math.atan2(hotspot.y - anchor.y, hotspot.x - anchor.x); 630 var attachX = hotspot.x - Math.cos(chordAim) * metrics.cuffReach; 631 var attachY = hotspot.y - Math.sin(chordAim) * metrics.cuffReach;
632 var armEndX = attachX + Math.cos(chordAim) * metrics.forearmOverlap; 633 var armEndY = attachY + Math.sin(chordAim) * metrics.forearmOverlap; 634 var wristAim = getWristTangent(anchor.x, anchor.y, armEndX, armEndY, hotspot.x, hotspot.y); 635 636 attachX = hotspot.x - Math.cos(wristAim) * metrics.cuffReach; 637 attachY = hotspot.y - Math.sin(wristAim) * metrics.cuffReach; 638 armEndX = attachX + Math.cos(wristAim) * metrics.forearmOverlap; 639 armEndY = attachY + Math.sin(wristAim) * metrics.forearmOverlap; 640 wristAim = getWristTangent(anchor.x, anchor.y, armEndX, armEndY, hotspot.x, hotspot.y); 641 642 return { 643 aim: wristAim, 644 attachX: attachX, 645 attachY: attachY, 646 armEndX: armEndX, 647 armEndY: armEndY, 648 sideX: hotspot.x, 649 sideY: hotspot.y 650 }; 651 } 652 653 function getPawAngle(layout) { 654 return layout.aim + Math.PI * 0.5; 655 } 656 657 function getWristSquish() { 658 if (!config.clickSquish || squish === 1) return 1; 659 return squish; 660 } 661 662 function getArmSquishFactor(t, wristSquish) { 663 if (wristSquish === 1) return 1; 664 var along = Math.pow(Math.max(0, Math.min(1, t)), 1.65); 665 return 1 + (wristSquish - 1) * along; 666 } 667 668 function getAnchorInwardNormal(ax, ay) { 669 var ix = global.innerWidth * 0.5 - ax; 670 var iy = global.innerHeight * 0.5 - ay; 671 var len = Math.hypot(ix, iy) || 1; 672 return { x: ix / len, y: iy / len }; 673 } 674 675 function getArmBendFactor(ax, ay, sideX, sideY) { 676 var dx = sideX - ax; 677 var dy = sideY - ay; 678 var len = Math.hypot(dx, dy) || 1; 679 var ux = dx / len; 680 var uy = dy / len; 681 var inward = getAnchorInwardNormal(ax, ay); 682 var cross = ux * inward.y - uy * inward.x; 683 var lateral = Math.abs(cross); 684 if (lateral < 0.0001) return 0; 685 var bendSign = cross > 0 ? 1 : -1; 686 return lateral * bendSign; 687 } 688 689 function getControlPoint(ax, ay, px, py, sideX, sideY) { 690 var mx = (ax + px) * 0.5; 691 var my = (ay + py) * 0.5; 692 var dx = px - ax; 693 var dy = py - ay; 694 var len = Math.hypot(dx, dy) || 1; 695 var nx = -dy / len; 696 var ny = dx / len; 697 var refX = sideX !== undefined ? sideX : px; 698 var refY = sideY !== undefined ? sideY : py; 699 var bendFactor = getArmBendFactor(ax, ay, refX, refY); 700 var bend = (config.armBend / 100) * Math.min(180, len * 0.35) * bendFactor; 701 return { x: mx + nx * bend, y: my + ny * bend }; 702 } 703 704 function quadPoint(t, x0, y0, cx, cy, x1, y1) { 705 var u = 1 - t; 706 return { 707 x: u * u * x0 + 2 * u * t * cx + t * t * x1, 708 y: u * u * y0 + 2 * u * t * cy + t * t * y1 709 }; 710 } 711 712 function sampleArmPath(ax, ay, wx, wy, steps, sideX, sideY) { 713 var cp = getControlPoint(ax, ay, wx, wy, sideX, sideY); 714 var pts = []; 715 var i; 716 for (i = 0; i <= steps; i++) { 717 pts.push(quadPoint(i / steps, ax, ay, cp.x, cp.y, wx, wy)); 718 } 719 return pts; 720 } 721 722 function armEdgeNoise(x, y, seed) { 723 var n = (x * 374761393 + y * 668265263 + seed * 982451653) | 0; 724 n = (n ^ (n >> 13)) * 1274126177; 725 return ((n ^ (n >> 16)) >>> 0) / 4294967295; 726 } 727 728 function drawFuzzyPixelArm(left, right, armColors) { 729 var i; 730 var pt; 731 var prev; 732 var next; 733 var tx; 734 var ty; 735 var len; 736 var nx; 737 var ny; 738 var noise; 739 var dist; 740 var fx; 741 var fy; 742 var snappedLeft = left.map(function (p) { 743 return { x: snapPixel(p.x), y: snapPixel(p.y) }; 744 }); 745 var snappedRight = right.map(function (p) { 746 return { x: snapPixel(p.x), y: snapPixel(p.y) }; 747 }); 748 749 var colors = armColors || SPRITE_ARM_COLORS; 750 ctx.imageSmoothingEnabled = false; 751 ctx.fillStyle = colors.fill; 752 ctx.beginPath(); 753 ctx.moveTo(snappedLeft[0].x, snappedLeft[0].y); 754 for (i = 1; i < snappedLeft.length; i++) ctx.lineTo(snappedLeft[i].x, snappedLeft[i].y); 755 for (i = snappedRight.length - 1; i >= 0; i--) ctx.lineTo(snappedRight[i].x, snappedRight[i].y); 756 ctx.closePath(); 757 ctx.fill(); 758 759 function fuzzEdge(chain, outward, seedBase) { 760 for (i = 0; i < chain.length; i++) { 761 pt = chain[i]; 762 if (i === 0) { 763 if (chain.length > 1) { 764 tx = chain[1].x - chain[0].x; 765 ty = chain[1].y - chain[0].y; 766 } else { 767 tx = 0; 768 ty = outward; 769 } 770 } else if (i === chain.length - 1) { 771 tx = chain[i].x - chain[i - 1].x; 772 ty = chain[i].y - chain[i - 1].y; 773 } else { 774 tx = chain[i + 1].x - chain[i - 1].x; 775 ty = chain[i + 1].y - chain[i - 1].y; 776 } 777 len = Math.hypot(tx, ty) || 1; 778 nx = (-ty / len) * outward; 779 ny = (tx / len) * outward; 780 781 noise = armEdgeNoise(pt.x, pt.y, seedBase + i); 782 ctx.fillStyle = noise < 0.42 ? colors.edge : colors.fill; 783 ctx.fillRect(pt.x, pt.y, 1, 1); 784 785 if (noise > 0.18) { 786 dist = noise > 0.78 ? 2 : 1; 787 fx = snapPixel(pt.x + nx * dist); 788 fy = snapPixel(pt.y + ny * dist); 789 ctx.fillStyle = noise > 0.62 ? colors.fill : colors.edge; 790 ctx.fillRect(fx, fy, 1, 1); 791 } 792 793 if (noise > 0.88 && i % 2 === 0) { 794 ctx.fillStyle = colors.fleck; 795 ctx.fillRect(snapPixel(pt.x + nx * 2), snapPixel(pt.y + ny * 2), 1, 1); 796 } 797 798 if (i > 0 && i < chain.length - 1 && noise > 0.52 && noise < 0.72) { 799 ctx.fillStyle = colors.edge; 800 ctx.fillRect(snapPixel(pt.x + nx), snapPixel(pt.y + ny), 1, 1); 801 } 802 } 803 } 804 805 fuzzEdge(snappedLeft, -1, 11); 806 fuzzEdge(snappedRight, 1, 29); 807 } 808 809 function drawPixelArmPolygon(left, right, fillColor, outlineColor) { 810 var i; 811 var snappedLeft = left.map(function (pt) { 812 return { x: snapPixel(pt.x), y: snapPixel(pt.y) }; 813 }); 814 var snappedRight = right.map(function (pt) { 815 return { x: snapPixel(pt.x), y: snapPixel(pt.y) }; 816 }); 817 818 ctx.fillStyle = fillColor; 819 ctx.beginPath(); 820 ctx.moveTo(snappedLeft[0].x, snappedLeft[0].y); 821 for (i = 1; i < snappedLeft.length; i++) ctx.lineTo(snappedLeft[i].x, snappedLeft[i].y); 822 for (i = snappedRight.length - 1; i >= 0; i--) ctx.lineTo(snappedRight[i].x, snappedRight[i].y); 823 ctx.closePath(); 824 ctx.fill(); 825 826 if (!config.outlineShow) return; 827 828 ctx.strokeStyle = outlineColor; 829 ctx.lineWidth = Math.max(1, snapPixel(BASE_OUTLINE * (config.outlineThickness / 100))); 830 ctx.lineJoin = 'miter'; 831 ctx.miterLimit = 1; 832 ctx.beginPath(); 833 ctx.moveTo(snappedLeft[0].x, snappedLeft[0].y); 834 for (i = 1; i < snappedLeft.length; i++) ctx.lineTo(snappedLeft[i].x, snappedLeft[i].y); 835 ctx.stroke(); 836 ctx.beginPath(); 837 ctx.moveTo(snappedRight[0].x, snappedRight[0].y); 838 for (i = 1; i < snappedRight.length; i++) ctx.lineTo(snappedRight[i].x, snappedRight[i].y); 839 ctx.stroke(); 840 } 841
842 function drawTaperedArm(ax, ay, endX, endY, cuffWidth, now, usePixelStyle, wristSquish, sideX, sideY) { 843 var sizeScale = config.pawSize / 100; 844 var taper = config.armTaper / 100; 845 var wristScale = wristSquish || 1; 846 var startW; 847 var endWBase; 848 if (usePixelStyle) { 849 endWBase = Math.max(cuffWidth, 6); 850 startW = Math.max(endWBase * 1.35, cuffWidth * 1.25, 8) * (1 - taper * 0.38); 851 } else { 852 startW = BASE_ARM_WIDTH * sizeScale; 853 endWBase = Math.max(cuffWidth, startW * (1 - taper * 0.45)); 854 } 855 var steps = usePixelStyle ? 20 : 24; 856 var path = sampleArmPath(ax, ay, endX, endY, steps, sideX, sideY); 857 var left = []; 858 var right = []; 859 var i; 860 var pt; 861 var t; 862 var tx; 863 var ty; 864 var len; 865 var nx; 866 var ny; 867 var w; 868 869 for (i = 0; i < path.length; i++) { 870 t = i / (path.length - 1); 871 pt = path[i]; 872 if (i === 0) { 873 tx = path[1].x - path[0].x; 874 ty = path[1].y - path[0].y; 875 } else if (i === path.length - 1) { 876 tx = path[i].x - path[i - 1].x; 877 ty = path[i].y - path[i - 1].y; 878 } else { 879 tx = path[i + 1].x - path[i - 1].x; 880 ty = path[i + 1].y - path[i - 1].y; 881 } 882 len = Math.hypot(tx, ty) || 1; 883 nx = -ty / len; 884 ny = tx / len; 885 w = (startW + (endWBase - startW) * t) * getArmSquishFactor(t, wristScale); 886 left.push({ x: pt.x + nx * w * 0.5, y: pt.y + ny * w * 0.5 }); 887 right.push({ x: pt.x - nx * w * 0.5, y: pt.y - ny * w * 0.5 }); 888 } 889 890 var armColor = resolveColor(config.arm, 0.2, now); 891 var outlineColor = resolveColor(config.outline, 0.5, now); 892 if (usePixelStyle) { 893 drawFuzzyPixelArm(left, right, getSpriteArmColors(now)); 894 return; 895 } 896 897 ctx.fillStyle = armColor; 898 ctx.beginPath(); 899 ctx.moveTo(left[0].x, left[0].y); 900 for (i = 1; i < left.length; i++) ctx.lineTo(left[i].x, left[i].y); 901 for (i = right.length - 1; i >= 0; i--) ctx.lineTo(right[i].x, right[i].y); 902 ctx.closePath(); 903 ctx.fill(); 904 905 if (config.outlineShow) { 906 ctx.strokeStyle = resolveColor(config.outline, 0.5, now); 907 ctx.lineWidth = BASE_OUTLINE * (config.outlineThickness / 100); 908 ctx.stroke(); 909 } 910 } 911 912 function drawForearmCuff(layout, metrics, now, wristSquish) { 913 var wristScale = wristSquish || 1; 914 var w = metrics.cuffWidth * wristScale; 915 var h = metrics.forearmOverlap * 0.95; 916 ctx.save(); 917 ctx.translate(layout.attachX, layout.attachY); 918 ctx.rotate(layout.aim); 919 if (metrics.useSprite) { 920 ctx.imageSmoothingEnabled = false; 921 var rw = Math.max(4, snapPixel(w)); 922 var rh = Math.max(3, snapPixel(h)); 923 var cx = snapPixel(metrics.forearmOverlap * 0.45); 924 ctx.fillStyle = SPRITE_ARM_COLORS.fill; 925 ctx.fillRect(cx - Math.round(rw * 0.5), -Math.round(rh * 0.5), rw, rh); 926 ctx.fillStyle = SPRITE_ARM_COLORS.edge; 927 ctx.fillRect(cx - Math.round(rw * 0.5), -Math.round(rh * 0.5), rw, 1); 928 ctx.fillRect(cx - Math.round(rw * 0.5), Math.round(rh * 0.5) - 1, rw, 1); 929 ctx.fillRect(cx - Math.round(rw * 0.5), -Math.round(rh * 0.5), 1, rh); 930 ctx.fillRect(cx + Math.round(rw * 0.5) - 1, -Math.round(rh * 0.5), 1, rh); 931 ctx.restore(); 932 return; 933 } 934 var armColor = resolveColor(config.arm, 0.2, now); 935 ctx.fillStyle = armColor; 936 ctx.beginPath(); 937 ctx.ellipse(metrics.forearmOverlap * 0.45, 0, w * 0.5, h * 0.5, 0, 0, Math.PI * 2); 938 ctx.fill(); 939 if (config.outlineShow) { 940 ctx.strokeStyle = resolveColor(config.outline, 0.5, now); 941 ctx.lineWidth = BASE_OUTLINE * (config.outlineThickness / 100); 942 ctx.stroke(); 943 }
944 ctx.restore(); 945 } 946 947 function drawBean(x, y, rx, ry, rot) { 948 ctx.save(); 949 ctx.translate(x, y); 950 ctx.rotate(rot); 951 ctx.beginPath(); 952 ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2); 953 ctx.fill(); 954 if (config.outlineShow) ctx.stroke(); 955 ctx.restore(); 956 } 957 958 function drawCatPawBody(metrics, furColor, armColor) { 959 var s = metrics.scale; 960 var ps = metrics.padScale; 961 ctx.fillStyle = furColor; 962 ctx.beginPath(); 963 ctx.moveTo(-10 * s * ps, 10 * s); 964 ctx.quadraticCurveTo(-11 * s, 4 * s, -8 * s * ps, -4 * s); 965 ctx.quadraticCurveTo(-4 * s, -12 * s * ps, 0, -13 * s * ps); 966 ctx.quadraticCurveTo(4 * s, -12 * s * ps, 8 * s * ps, -4 * s); 967 ctx.quadraticCurveTo(11 * s, 4 * s, 10 * s * ps, 10 * s); 968 ctx.quadraticCurveTo(6 * s, 14 * s, 0, 14 * s); 969 ctx.quadraticCurveTo(-6 * s, 14 * s, -10 * s * ps, 10 * s); 970 ctx.closePath(); 971 ctx.fill(); 972 if (config.outlineShow) ctx.stroke(); 973 974 ctx.fillStyle = armColor; 975 ctx.beginPath(); 976 ctx.moveTo(-9 * s * ps, 10 * s); 977 ctx.quadraticCurveTo(-8 * s, 16 * s, 0, 17 * s); 978 ctx.quadraticCurveTo(8 * s, 16 * s, 9 * s * ps, 10 * s); 979 ctx.closePath(); 980 ctx.fill(); 981 if (config.outlineShow) ctx.stroke(); 982 } 983 984 function drawDogPawBody(metrics, furColor, armColor) { 985 var s = metrics.scale; 986 var ps = metrics.padScale; 987 ctx.fillStyle = furColor; 988 ctx.beginPath(); 989 ctx.moveTo(-12 * s * ps, 11 * s); 990 ctx.quadraticCurveTo(-13 * s, 3 * s, -9 * s * ps, -5 * s); 991 ctx.quadraticCurveTo(-5 * s, -13 * s * ps, 0, -14 * s * ps); 992 ctx.quadraticCurveTo(5 * s, -13 * s * ps, 9 * s * ps, -5 * s); 993 ctx.quadraticCurveTo(13 * s, 3 * s, 12 * s * ps, 11 * s); 994 ctx.quadraticCurveTo(7 * s, 15 * s, 0, 15 * s); 995 ctx.quadraticCurveTo(-7 * s, 15 * s, -12 * s * ps, 11 * s); 996 ctx.closePath(); 997 ctx.fill(); 998 if (config.outlineShow) ctx.stroke(); 999 1000 ctx.fillStyle = armColor; 1001 ctx.beginPath(); 1002 ctx.moveTo(-10 * s * ps, 11 * s); 1003 ctx.quadraticCurveTo(-9 * s, 17 * s, 0, 18 * s); 1004 ctx.quadraticCurveTo(9 * s, 17 * s, 10 * s * ps, 11 * s); 1005 ctx.closePath(); 1006 ctx.fill(); 1007 if (config.outlineShow) ctx.stroke(); 1008 } 1009 1010 function drawCatPads(metrics, padColor) { 1011 var s = metrics.scale; 1012 var ps = metrics.padScale; 1013 ctx.fillStyle = padColor; 1014 var toes = [ 1015 [-6.5, -8.5, 2.8, 3.1, -0.3], 1016 [-2.2, -11, 2.6, 3, 0], 1017 [2.2, -11, 2.6, 3, 0], 1018 [6.5, -8.5, 2.8, 3.1, 0.3] 1019 ]; 1020 toes.forEach(function (toe) { 1021 drawBean(toe[0] * s * ps, toe[1] * s * ps, toe[2] * s * ps, toe[3] * s * ps, toe[4]); 1022 }); 1023 ctx.beginPath(); 1024 ctx.ellipse(0, 4.5 * s * ps, 5 * s * ps, 4.2 * s * ps, 0, 0, Math.PI * 2); 1025 ctx.fill(); 1026 if (config.outlineShow) ctx.stroke(); 1027 } 1028 1029 function drawDogPads(metrics, padColor) { 1030 var s = metrics.scale; 1031 var ps = metrics.padScale; 1032 ctx.fillStyle = padColor; 1033 var toes = [ 1034 [-8, -9, 3, 2.5, -0.2], 1035 [-2.8, -11.5, 2.8, 2.3, 0], 1036 [2.8, -11.5, 2.8, 2.3, 0], 1037 [8, -9, 3, 2.5, 0.2] 1038 ]; 1039 toes.forEach(function (toe) { 1040 drawBean(toe[0] * s * ps, toe[1] * s * ps, toe[2] * s * ps, toe[3] * s * ps, toe[4]); 1041 }); 1042 ctx.beginPath(); 1043 ctx.ellipse(0, 5.5 * s * ps, 5.8 * s * ps, 4.6 * s * ps, 0, 0, Math.PI * 2); 1044 ctx.fill(); 1045 if (config.outlineShow) ctx.stroke(); 1046 } 1047 1048 function drawProceduralPaw(hotspotX, hotspotY, layout, metrics, now) { 1049 var furColor = resolveColor(config.pawFur, 0.1, now); 1050 var padColor = resolveColor(config.pawPads, 0.6, now); 1051 var armColor = resolveColor(config.arm, 0.2, now); 1052 1053 ctx.save(); 1054 ctx.translate(hotspotX, hotspotY); 1055 ctx.rotate(getPawAngle(layout)); 1056 ctx.scale(squish, 2 - squish); 1057 ctx.translate(0, metrics.hotspotOffset); 1058
1059 ctx.strokeStyle = resolveColor(config.outline, 0.5, now); 1060 ctx.lineWidth = BASE_OUTLINE * (config.outlineThickness / 100); 1061 ctx.lineJoin = 'round'; 1062 1063 if (config.pawSpecies === 'dog') { 1064 if (config.showBeans !== false) drawDogPads(metrics, padColor); 1065 drawDogPawBody(metrics, furColor, armColor); 1066 } else { 1067 if (config.showBeans !== false) drawCatPads(metrics, padColor); 1068 drawCatPawBody(metrics, furColor, armColor); 1069 } 1070 1071 ctx.restore(); 1072 } 1073 1074 function drawSpritePaw(hotspotX, hotspotY, layout, metrics, now) { 1075 var presetId = getActivePresetId(); 1076 var baseImg = getTintedSprite(now); 1077 var beansImg = pawSpriteBeans[presetId]; 1078 var dims = getSpriteDimensions(); 1079 var meta = metrics.spriteMeta || getPawSpriteMeta(presetId); 1080 if (!baseImg || !dims) { 1081 drawProceduralPaw(hotspotX, hotspotY, layout, metrics, now); 1082 return; 1083 } 1084 1085 var pawAngle = getPawAngle(layout); 1086 var wristScale = getWristSquish(); 1087 var drawX = -dims.width * 0.5; 1088 var drawY = -dims.height * meta.wristY; 1089 1090 ctx.save(); 1091 ctx.imageSmoothingEnabled = false; 1092 ctx.translate(layout.attachX, layout.attachY); 1093 ctx.rotate(pawAngle); 1094 if (config.clickSquish && wristScale !== 1) { 1095 ctx.scale(wristScale, wristScale); 1096 } 1097 ctx.drawImage(baseImg, drawX, drawY, dims.width, dims.height); 1098 if (config.showBeans !== false && beansImg && beansImg.naturalWidth) { 1099 ctx.drawImage(beansImg, drawX, drawY, dims.width, dims.height); 1100 } 1101 ctx.restore(); 1102 } 1103 1104 function drawUnifiedPaw(hotspotX, hotspotY, layout, metrics, now) { 1105 if (metrics.useSprite) { 1106 drawSpritePaw(hotspotX, hotspotY, layout, metrics, now); 1107 return; 1108 } 1109 drawProceduralPaw(hotspotX, hotspotY, layout, metrics, now); 1110 } 1111 1112 function drawClickHotspot(x, y, now) { 1113 var outline = resolveColor(config.outline, 0.5, now); 1114 ctx.save(); 1115 ctx.strokeStyle = outline; 1116 ctx.fillStyle = 'rgba(255,255,255,0.92)'; 1117 ctx.lineWidth = 1.4; 1118 1119 ctx.beginPath(); 1120 ctx.arc(x, y, 3.2, 0, Math.PI * 2); 1121 ctx.fill(); 1122 ctx.stroke(); 1123 1124 ctx.globalAlpha = 0.75; 1125 ctx.beginPath(); 1126 ctx.moveTo(x - 5, y); 1127 ctx.lineTo(x + 5, y); 1128 ctx.moveTo(x, y - 5); 1129 ctx.lineTo(x, y + 5); 1130 ctx.stroke(); 1131 ctx.restore(); 1132 } 1133 1134 function spawnPrint(x, y) { 1135 if (!config.printsEnabled || !printLayer) return; 1136 var max = config.printMax; 1137 while (prints.length >= max) { 1138 var old = prints.shift(); 1139 if (old && old.el && old.el.parentNode) old.el.remove(); 1140 } 1141 var el = document.createElement('div'); 1142 var presetId = getActivePresetId(); 1143 var spritePath = getPawSpritePath(presetId); 1144 var beansPath = getPawSpriteBeansPath(presetId); 1145 el.className = 'cat-paw-print' + (shouldUsePawSprite() ? ' cat-paw-print--sprite' : ''); 1146 el.setAttribute('aria-hidden', 'true'); 1147 var size = 28 * (config.printSize / 100); 1148 var fadeMs = PRINT_BASE_MS * (config.printFade / 100); 1149 el.style.width = size + 'px'; 1150 el.style.height = size + 'px'; 1151 el.style.left = (x - size * 0.5) + 'px'; 1152 el.style.top = (y - size * 0.5) + 'px'; 1153 el.style.setProperty('--cat-paw-print-fade', fadeMs + 'ms'); 1154 el.dataset.species = config.pawPreset || config.pawSpecies; 1155 if (shouldUsePawSprite() && spritePath) { 1156 if (config.showBeans !== false && beansPath) { 1157 el.style.backgroundImage = 'url("' + beansPath + '"), url("' + spritePath + '")'; 1158 el.style.backgroundSize = 'contain, contain'; 1159 el.style.backgroundRepeat = 'no-repeat, no-repeat'; 1160 el.style.backgroundPosition = 'center, center'; 1161 } else { 1162 el.style.backgroundImage = 'url("' + spritePath + '")'; 1163 el.style.backgroundSize = 'contain'; 1164 el.style.backgroundRepeat = 'no-repeat'; 1165 el.style.backgroundPosition = 'center'; 1166 } 1167 } else {
1168 el.style.color = resolveColor(config.prints, 0.5, performance.now()); 1169 } 1170 printLayer.appendChild(el); 1171 prints.push({ el: el }); 1172 global.setTimeout(function () { 1173 if (el.parentNode) el.remove(); 1174 prints = prints.filter(function (p) { return p.el !== el; }); 1175 }, fadeMs + 80); 1176 } 1177 1178 function ensureStyles() { 1179 if (document.getElementById('cat-paw-arm-style')) return; 1180 var style = document.createElement('style'); 1181 style.id = 'cat-paw-arm-style'; 1182 style.textContent = 1183 'html.cat-paw-arm-active, html.cat-paw-arm-active *, ' + 1184 'body.cat-paw-arm-active, body.cat-paw-arm-active * { cursor: none !important; }' + 1185 'body.cat-paw-arm-active #settings-panel, body.cat-paw-arm-active #settings-panel *, ' + 1186 'body.cat-paw-arm-active #settings-backdrop, body.cat-paw-arm-active .settings-dialog-backdrop, ' + 1187 'body.cat-paw-arm-active .settings-dialog-backdrop * { cursor: auto !important; }' + 1188 'body.cat-paw-arm-active #settings-panel button, body.cat-paw-arm-active #settings-panel .settings-switch__input, ' + 1189 'body.cat-paw-arm-active #settings-panel .settings-switch__track, body.cat-paw-arm-active #settings-panel label { cursor: pointer !important; }' + 1190 '#cat-paw-arm-overlay, #cat-paw-arm-overlay * { pointer-events: none !important; touch-action: none !important; }' + 1191 '#cat-paw-print-layer { position: fixed; inset: 0; pointer-events: none; z-index: 101598; overflow: hidden; }' + 1192 '.cat-paw-print--sprite { opacity: 0.72; image-rendering: pixelated; image-rendering: crisp-edges; }' + 1193 '.cat-paw-print--sprite::before, .cat-paw-print--sprite::after { content: none !important; display: none !important; }' + 1194 '.cat-paw-print { position: fixed; pointer-events: none; opacity: 0.62; transform: scale(1); ' + 1195 'animation: catPawPrintFade var(--cat-paw-print-fade, 3s) ease-out forwards; }' + 1196 '.cat-paw-print::before { content: ""; position: absolute; inset: 0; background: currentColor; ' + 1197 'mask-image: radial-gradient(ellipse 38% 34% at 50% 58%, #000 96%, transparent 97%); ' + 1198 '-webkit-mask-image: radial-gradient(ellipse 38% 34% at 50% 58%, #000 96%, transparent 97%); }' + 1199 '.cat-paw-print[data-species="cat"]::after { content: ""; position: absolute; inset: 0; background: currentColor; ' + 1200 'mask: radial-gradient(circle at 26% 24%, #000 18%, transparent 19%), radial-gradient(circle at 74% 24%, #000 18%, transparent 19%), ' + 1201 'radial-gradient(circle at 38% 12%, #000 16%, transparent 17%), radial-gradient(circle at 62% 12%, #000 16%, transparent 17%); ' + 1202 '-webkit-mask: radial-gradient(circle at 26% 24%, #000 18%, transparent 19%), radial-gradient(circle at 74% 24%, #000 18%, transparent 19%), ' + 1203 'radial-gradient(circle at 38% 12%, #000 16%, transparent 17%), radial-gradient(circle at 62% 12%, #000 16%, transparent 17%); }' + 1204 '.cat-paw-print[data-species="dog"]::after { content: ""; position: absolute; inset: 0; background: currentColor; ' + 1205 'mask: radial-gradient(ellipse 16% 12% at 18% 28%, #000 96%, transparent 97%), radial-gradient(ellipse 16% 12% at 82% 28%, #000 96%, transparent 97%), ' + 1206 'radial-gradient(ellipse 14% 11% at 36% 14%, #000 96%, transparent 97%), radial-gradient(ellipse 14% 11% at 64% 14%, #000 96%, transparent 97%); ' + 1207 '-webkit-mask: radial-gradient(ellipse 16% 12% at 18% 28%, #000 96%, transparent 97%), radial-gradient(ellipse 16% 12% at 82% 28%, #000 96%, transparent 97%), ' + 1208 'radial-gradient(ellipse 14% 11% at 36% 14%, #000 96%, transparent 97%), radial-gradient(ellipse 14% 11% at 64% 14%, #000 96%, transparent 97%); }' + 1209 '@keyframes catPawPrintFade { 0% { opacity: 0.62; transform: scale(1); } 100% { opacity: 0; transform: scale(0.82); } }' + 1210 '@media (prefers-reduced-motion: reduce) { .cat-paw-print { animation-duration: 0.01ms; } }'; 1211 document.head.appendChild(style); 1212 } 1213 1214 function setCursorActive(active) { 1215 document.documentElement.classList.toggle('cat-paw-arm-active', active); 1216 document.body.classList.toggle('cat-paw-arm-active', active); 1217 } 1218 1219 function onResize() { 1220 if (!canvas) return; 1221 canvas.width = global.innerWidth;
1222 canvas.height = global.innerHeight; 1223 } 1224 1225 var downLock = false; 1226 1227 function onMouseMove(e) { 1228 target.x = e.clientX; 1229 target.y = e.clientY; 1230 global.__pupwifeLastMouseX = e.clientX; 1231 global.__pupwifeLastMouseY = e.clientY; 1232 } 1233 1234 function onPointerMove(e) { 1235 if (e.pointerType === 'touch') return; 1236 onMouseMove(e); 1237 } 1238 1239 function onMouseDown(e) { 1240 if (downLock) return; 1241 downLock = true; 1242 if (config.clickSquish && !prefersReducedMotion()) { 1243 squishTarget = 0.82; 1244 } 1245 if (config.printsEnabled) spawnPrint(e.clientX, e.clientY); 1246 } 1247 1248 function onPointerDown(e) { 1249 if (e.pointerType === 'touch') return; 1250 onMouseDown(e); 1251 } 1252 1253 function onMouseUp() { 1254 downLock = false; 1255 squishTarget = 1; 1256 } 1257 1258 function tick(now) { 1259 if (!isActive) return; 1260 var reduced = prefersReducedMotion(); 1261 var lag = config.followLag / 100; 1262 var follow = reduced ? 1 : Math.max(0.12, 0.38 - lag * 0.0026); 1263 display.x = lerp(display.x, target.x, follow); 1264 display.y = lerp(display.y, target.y, follow); 1265 squish = lerp(squish, squishTarget, reduced ? 1 : 0.28); 1266 1267 var anchor = getAnchor(); 1268 var metrics = getPawMetrics(); 1269 var pawPoint = { x: target.x, y: target.y }; 1270 var layout = getPawLayout(anchor, pawPoint, metrics); 1271 var pixelStyle = !!metrics.useSprite; 1272 1273 ctx.clearRect(0, 0, canvas.width, canvas.height); 1274 ctx.imageSmoothingEnabled = !pixelStyle; 1275 1276 var wristSquish = getWristSquish(); 1277 drawTaperedArm(anchor.x, anchor.y, layout.armEndX, layout.armEndY, metrics.cuffWidth, now, pixelStyle, wristSquish, layout.sideX, layout.sideY); 1278 if (!pixelStyle) drawForearmCuff(layout, metrics, now, wristSquish); 1279 drawUnifiedPaw(pawPoint.x, pawPoint.y, layout, metrics, now); 1280 if (!pixelStyle) drawClickHotspot(target.x, target.y, now); 1281 1282 animId = global.requestAnimationFrame(tick); 1283 } 1284 1285 function onSettingChanged(e) { 1286 if (!e || !e.detail) return; 1287 if (e.detail.key === 'catPawArm' || e.detail.key === 'customCursor' || e.detail.key === 'performanceMode') { 1288 if (isPerformanceMode()) { 1289 setCursor('none'); 1290 return; 1291 } 1292 if (global.SettingsMenu && global.SettingsMenu.getSetting('customCursor') !== 'cat-paw-arm') { 1293 if (isActive) setCursor('none'); 1294 return; 1295 } 1296 applySettings(readConfig()); 1297 if (!isActive && global.SettingsMenu && global.SettingsMenu.getSetting('customCursor') === 'cat-paw-arm') { 1298 setCursor('cat-paw-arm'); 1299 } 1300 } 1301 } 1302 1303 function applySettings(next) { 1304 config = mergeConfig(next); 1305 clearTintedSpriteCache(); 1306 if (getActivePresetId()) preloadPawSprite(getActivePresetId()); 1307 if (!isActive) return; 1308 if (printLayer) { 1309 prints.forEach(function (p) { 1310 if (p.el && p.el.parentNode) p.el.remove(); 1311 }); 1312 prints = []; 1313 } 1314 } 1315 1316 function initOverlay() { 1317 ensureStyles(); 1318 preloadPawSprites(); 1319 if (!overlay) { 1320 overlay = document.createElement('div'); 1321 overlay.id = 'cat-paw-arm-overlay'; 1322 overlay.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:101600;'; 1323 canvas = document.createElement('canvas'); 1324 canvas.style.cssText = 'width:100%;height:100%;display:block;image-rendering:pixelated;image-rendering:crisp-edges;'; 1325 overlay.appendChild(canvas); 1326 printLayer = document.createElement('div'); 1327 printLayer.id = 'cat-paw-print-layer'; 1328 document.body.appendChild(printLayer); 1329 document.body.appendChild(overlay); 1330 ctx = canvas.getContext('2d'); 1331 } 1332 onResize(); 1333 } 1334 1335 function bindEvents() { 1336 global.addEventListener('mousemove', onMouseMove, { passive: true }); 1337 global.addEventListener('pointermove', onPointerMove, { passive: true }); 1338 global.addEventListener('mousedown', onMouseDown, { passive: true }); 1339 global.addEventListener('pointerdown', onPointerDown, { passive: true }); 1340 global.addEventListener('mouseup', onMouseUp, { passive: true }); 1341 global.addEventListener('pointerup', onMouseUp, { passive: true }); 1342 global.addEventListener('resize', onResize); 1343 document.addEventListener('pupwife:setting-changed', onSettingChanged); 1344 } 1345 1346 function unbindEvents() { 1347 global.removeEventListener('mousemove', onMouseMove); 1348 global.removeEventListener('pointermove', onPointerMove); 1349 global.removeEventListener('mousedown', onMouseDown); 1350 global.removeEventListener('pointerdown', onPointerDown); 1351 global.removeEventListener('mouseup', onMouseUp); 1352 global.removeEventListener('pointerup', onMouseUp); 1353 global.removeEventListener('resize', onResize); 1354 document.removeEventListener('pupwife:setting-changed', onSettingChanged); 1355 } 1356 1357 function cleanup() { 1358 isActive = false;
1359 if (animId) { 1360 global.cancelAnimationFrame(animId); 1361 animId = null; 1362 } 1363 unbindEvents(); 1364 setCursorActive(false); 1365 if (prints.length) { 1366 prints.forEach(function (p) { 1367 if (p.el && p.el.parentNode) p.el.remove(); 1368 }); 1369 prints = []; 1370 } 1371 if (overlay && overlay.parentNode) overlay.remove(); 1372 if (printLayer && printLayer.parentNode) printLayer.remove(); 1373 overlay = null; 1374 canvas = null; 1375 printLayer = null; 1376 ctx = null; 1377 } 1378 1379 function setCursor(type) { 1380 if (type !== 'cat-paw-arm' || isPerformanceMode() || isCoarsePointer()) { 1381 cleanup(); 1382 return; 1383 } 1384 if (isActive) { 1385 applySettings(readConfig()); 1386 return; 1387 } 1388 config = readConfig(); 1389 initOverlay(); 1390 bindEvents(); 1391 isActive = true; 1392 setCursorActive(true); 1393 target.x = typeof global.__pupwifeLastMouseX === 'number' ? global.__pupwifeLastMouseX : global.innerWidth / 2; 1394 target.y = typeof global.__pupwifeLastMouseY === 'number' ? global.__pupwifeLastMouseY : global.innerHeight / 2; 1395 display.x = target.x; 1396 display.y = target.y; 1397 squish = 1; 1398 squishTarget = 1; 1399 if (animId) global.cancelAnimationFrame(animId); 1400 animId = global.requestAnimationFrame(tick); 1401 emitDevLog('Stretchy paw cursor active'); 1402 } 1403 1404 function readSavedCursor() { 1405 try { 1406 var raw = global.localStorage && global.localStorage.getItem('puppywifey_settings'); 1407 if (!raw) return null; 1408 var parsed = JSON.parse(raw); 1409 if (parsed.customCursor === 'cat-paw-arm') return 'cat-paw-arm'; 1410 } catch (e) { /* ignore */ } 1411 return null; 1412 } 1413 1414 global.CatPawArmCursor = { 1415 setCursor: setCursor, 1416 applySettings: applySettings, 1417 mergeConfig: mergeConfig, 1418 defaultConfig: defaultConfig, 1419 ANCHOR_PRESETS: ANCHOR_PRESETS, 1420 PAW_PRESETS: PAW_PRESETS, 1421 PAW_SPRITE_PATHS: PAW_SPRITE_PATHS, 1422 PAW_SPRITE_META: PAW_SPRITE_META, 1423 getPresetOptions: getPresetOptions, 1424 hasSpriteArt: hasSpriteArt, 1425 hasPresetArt: hasPresetArt, 1426 cleanup: cleanup 1427 }; 1428 1429 preloadPawSprites(); 1430 1431 document.addEventListener('pupwife:apply-custom-cursor', function () { 1432 var saved = readSavedCursor(); 1433 if (saved) setCursor(saved); 1434 }); 1435})(typeof window !== 'undefined' ? window : this);
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.