1/** 2 * Rough.js custom cursor system (hand-drawn sketch cursors). 3 */ 4(function (global) { 5 'use strict'; 6 7 var ROUGH_JS_SRC = 'https://cdn.jsdelivr.net/npm/[email protected]/bundled/rough.js'; 8 var ROUGH_LIB_MARK = 'data-pupwife-roughjs-lib'; 9 10 var CURSOR_SPECS = { 11 'rough-drawn': { 12 canvasW: 34, 13 canvasH: 34, 14 hotspotX: 12, 15 hotspotY: 12, 16 clickScale: 0.9, 17 draw: drawDrawnPointer 18 }, 19 'rough-paw': { 20 canvasW: 32, 21 canvasH: 36, 22 hotspotX: 16, 23 hotspotY: 20, 24 clickScale: 0.88, 25 showClickMarker: true, 26 draw: drawSketchPaw 27 }, 28 'rough-pencil': { 29 canvasW: 30, 30 canvasH: 32, 31 hotspotX: 10, 32 hotspotY: 10, 33 clickScale: 0.92, 34 showClickMarker: true, 35 draw: drawPencilNib 36 }, 37 'rough-heart': { 38 canvasW: 30, 39 canvasH: 32, 40 hotspotX: 15, 41 hotspotY: 24, 42 clickScale: 0.9, 43 showClickMarker: true, 44 draw: drawHeartPointer 45 } 46 }; 47 48 var CURSOR_TYPES = { none: null }; 49 Object.keys(CURSOR_SPECS).forEach(function (key) { 50 CURSOR_TYPES[key] = key; 51 }); 52 53 var ARROW_POINTS = [ 54 [0, 0], [0, 18], [5, 13], [8, 22], [10, 13], [18, 10] 55 ]; 56 57 var HAND_POINTS = [ 58 [-3, 1], [0, 0], [2, 3], [1, 9], [4, 13], [3, 19], 59 [-1, 21], [-5, 20], [-8, 17], [-10, 13], [-11, 9], [-9, 5], [-6, 3] 60 ]; 61 62 var PAW_BEANS_DEFAULT = [ 63 [-7, -9], [-2.5, -11], [2.5, -11], [7, -9] 64 ]; 65 66 var PAW_BEANS_POINTER = [ 67 [-9, -7], [-3, -10], [3, -10], [9, -7] 68 ]; 69 70 var HEART_DEFAULT = [ 71 [0, 0], [-9, -5], [-10, -12], [-4, -16], [0, -13], 72 [4, -16], [10, -12], [9, -5] 73 ]; 74 75 var HEART_POINTER = [ 76 [0, 0], [-8, -4], [-9, -11], [-3, -14], [0, -11], 77 [5, -15], [12, -10], [14, -4], [8, -2] 78 ]; 79 80 var currentCursor = null; 81 var currentCursorType = 'none'; 82 var currentSpec = null; 83 var canvas = null; 84 var roughCanvas = null; 85 var animId = null; 86 var roughLoadPromise = null; 87 var pendingCursorType = null; 88 89 function paintSeed(specId, paintKey) { 90 var s = String(specId) + String(paintKey); 91 var h = 0; 92 for (var i = 0; i < s.length; i++) { 93 h = ((h << 5) - h + s.charCodeAt(i)) | 0; 94 } 95 return Math.abs(h) || 1; 96 } 97 98 function getInkOptions(seed) { 99 var dark = isDarkMode(); 100 return { 101 seed: seed, 102 stroke: dark ? '#ece6dc' : '#2e2e2e', 103 fill: dark ? '#3a3644' : '#faf7f2', 104 roughness: 0.95, 105 bowing: 0.85, 106 strokeWidth: 1.25, 107 fillStyle: 'solid', 108 fillWeight: 0.35, 109 disableMultiStroke: true 110 }; 111 } 112 113 function getAccentOptions(base) { 114 return { 115 seed: base.seed, 116 stroke: base.stroke, 117 fill: base.stroke, 118 roughness: 1.1, 119 bowing: 1, 120 strokeWidth: 0.85, 121 fillStyle: 'solid', 122 fillWeight: 0.3, 123 disableMultiStroke: true 124 }; 125 } 126 127 function drawClickMarker(cursor) { 128 if (!cursor.spec.showClickMarker) return; 129 var ctx = cursor.canvas.getContext('2d'); 130 if (!ctx) return; 131 132 var hx = cursor.spec.hotspotX; 133 var hy = cursor.spec.hotspotY; 134 var dark = isDarkMode(); 135 var accent = dark ? '#c8a8ff' : '#764ba2'; 136 137 ctx.save(); 138 ctx.strokeStyle = accent; 139 ctx.fillStyle = accent; 140 ctx.lineWidth = 1.15; 141 ctx.lineCap = 'round'; 142 ctx.lineJoin = 'round'; 143 144 ctx.beginPath(); 145 ctx.moveTo(hx - 4, hy); 146 ctx.lineTo(hx + 4, hy); 147 ctx.moveTo(hx, hy - 4); 148 ctx.lineTo(hx, hy + 4); 149 ctx.stroke(); 150 151 ctx.beginPath(); 152 ctx.moveTo(hx, hy); 153 ctx.lineTo(hx - 2.5, hy - 4.5); 154 ctx.moveTo(hx, hy); 155 ctx.lineTo(hx + 2.5, hy - 4.5); 156 ctx.stroke(); 157 158 ctx.beginPath(); 159 ctx.arc(hx, hy, cursor.isClicking ? 1.45 : 1.15, 0, Math.PI * 2); 160 ctx.fill();
161 ctx.restore(); 162 } 163 164 function emitDevLog() { 165 if (typeof global.devLog === 'function') { 166 global.devLog.apply(global, arguments); 167 } 168 } 169 170 function emitDevWarn() { 171 if (typeof global.devWarn === 'function') { 172 global.devWarn.apply(global, arguments); 173 } 174 } 175 176 function isDarkMode() { 177 var html = document.documentElement; 178 var body = document.body; 179 return html.classList.contains('dark-mode') || 180 body.classList.contains('dark-mode') || 181 html.getAttribute('data-theme') === 'dark'; 182 } 183 184 function offsetPoints(ox, oy, points) { 185 return points.map(function (pt) { 186 return [pt[0] + ox, pt[1] + oy]; 187 }); 188 } 189 190 function isInteractive(el) { 191 if (!el || el === document.documentElement || el === document.body) return false; 192 var tag = el.tagName; 193 if (tag === 'A' || tag === 'BUTTON' || tag === 'SELECT' || tag === 'OPTION') return true; 194 if (tag === 'INPUT') { 195 var type = (el.getAttribute('type') || 'text').toLowerCase(); 196 return type === 'button' || type === 'submit' || type === 'reset' || type === 'checkbox' || type === 'radio'; 197 } 198 if (el.getAttribute('role') === 'button' || el.getAttribute('role') === 'link') return true; 199 if (el.onclick || el.hasAttribute('onclick')) return true; 200 if (el.classList.contains('clickable') || el.classList.contains('tool-button')) return true; 201 if (el.classList.contains('settings-bubble-select__trigger') || el.classList.contains('settings-bubble-select__option')) return true; 202 return false; 203 } 204 205 function findInteractiveAt(x, y) { 206 var el = document.elementFromPoint(x, y); 207 var depth = 0; 208 while (el && depth < 8) { 209 if (isInteractive(el)) return true; 210 el = el.parentElement; 211 depth++; 212 } 213 return false; 214 } 215 216 function drawDrawnPointer(cursor) { 217 var spec = cursor.spec; 218 var ox = spec.hotspotX; 219 var oy = spec.hotspotY; 220 var opts = cursor.getInkOpts(); 221 var points = offsetPoints(ox, oy, cursor.isPointer ? HAND_POINTS : ARROW_POINTS); 222 cursor.rc.polygon(points, opts); 223 224 if (!spec.showClickMarker) { 225 var tipOpts = getAccentOptions(opts); 226 cursor.rc.circle(ox, oy, cursor.isClicking ? 1.6 : 1.2, tipOpts); 227 } 228 229 if (!cursor.isPointer) { 230 var shadeOpts = { 231 seed: opts.seed, 232 roughness: 1.4, 233 bowing: 0.8, 234 stroke: opts.stroke, 235 strokeWidth: 0.6, 236 disableMultiStroke: true, 237 disableMultiStrokeFill: true 238 }; 239 cursor.rc.line(ox + 2, oy + 14, ox + 6, oy + 11, shadeOpts); 240 cursor.rc.line(ox + 2, oy + 16, ox + 5, oy + 13, shadeOpts); 241 } 242 243 drawClickMarker(cursor); 244 } 245 246 function drawSketchPaw(cursor) { 247 var spec = cursor.spec; 248 var ox = spec.hotspotX; 249 var oy = spec.hotspotY; 250 var opts = cursor.getInkOpts(); 251 var beans = cursor.isPointer ? PAW_BEANS_POINTER : PAW_BEANS_DEFAULT; 252 var squish = cursor.isClicking ? 1.15 : 1; 253 254 cursor.rc.circle(ox, oy, 8 * squish, opts); 255 256 beans.forEach(function (bean) { 257 cursor.rc.circle(ox + bean[0], oy + bean[1], cursor.isClicking ? 3.8 : 3.4, opts); 258 }); 259 260 drawClickMarker(cursor); 261 } 262 263 function drawPencilNib(cursor) { 264 var spec = cursor.spec; 265 var ox = spec.hotspotX; 266 var oy = spec.hotspotY; 267 var opts = cursor.getInkOpts(); 268 269 cursor.rc.polygon(offsetPoints(ox, oy, [ 270 [0, 0], [0, 14], [5, 10] 271 ]), opts); 272 273 cursor.rc.line(ox + 5, oy + 10, ox + 17, oy + 22, { 274 seed: opts.seed, 275 roughness: 1.2, 276 bowing: 1, 277 stroke: opts.stroke, 278 strokeWidth: 1.4, 279 disableMultiStroke: true 280 }); 281 282 cursor.rc.rectangle(ox + 12, oy + 18, 9, 7, { 283 seed: opts.seed, 284 roughness: 1.3, 285 bowing: 0.9, 286 stroke: opts.stroke, 287 strokeWidth: 1, 288 fill: opts.fill, 289 fillStyle: 'solid', 290 fillWeight: 0.35, 291 disableMultiStroke: true 292 }); 293 294 var shadeOpts = { 295 seed: opts.seed, 296 roughness: 1.3, 297 bowing: 0.7, 298 stroke: opts.stroke, 299 strokeWidth: 0.55, 300 disableMultiStroke: true, 301 disableMultiStrokeFill: true 302 }; 303 cursor.rc.line(ox + 7, oy + 13, ox + 14, oy + 20, shadeOpts); 304 cursor.rc.line(ox + 9, oy + 15, ox + 15, oy + 21, shadeOpts); 305 306 if (cursor.smudgeLevel > 0) { 307 var smudgeOpts = getAccentOptions(opts); 308 for (var i = 0; i < cursor.smudgeLevel; i++) { 309 cursor.rc.circle( 310 ox - 2 - i * 2, 311 oy + 4 + i * 2, 312 0.8 + i * 0.2, 313 smudgeOpts 314 ); 315 } 316 } 317 318 drawClickMarker(cursor); 319 } 320 321 function drawHeartPointer(cursor) { 322 var spec = cursor.spec; 323 var ox = spec.hotspotX; 324 var oy = spec.hotspotY; 325 var opts = cursor.getInkOpts(); 326 var dark = isDarkMode(); 327 var heartOpts = Object.assign({}, opts, { 328 stroke: cursor.isPointer ? '#c8a8ff' : opts.stroke, 329 fill: cursor.isPointer ? (dark ? '#4a3d5c' : '#f3ebff') : opts.fill 330 }); 331 332 var points = offsetPoints(ox, oy, cursor.isPointer ? HEART_POINTER : HEART_DEFAULT); 333 cursor.rc.polygon(points, heartOpts); 334 335 drawClickMarker(cursor); 336 } 337 338 function RoughSketchCursor(spec, targetCanvas, rc, cursorTypeKey) { 339 this.cursorTypeKey = cursorTypeKey || 'rough-sketch'; 340 this.spec = spec; 341 this.canvas = targetCanvas; 342 this.rc = rc; 343 this.mouseX = typeof global.__pupwifeLastMouseX === 'number' ? global.__pupwifeLastMouseX : global.innerWidth / 2; 344 this.mouseY = typeof global.__pupwifeLastMouseY === 'number' ? global.__pupwifeLastMouseY : global.innerHe
344ight / 2; 345 this.speed = 0; 346 this.smudgeLevel = 0; 347 this.isPointer = false; 348 this.isClicking = false; 349 this.dirty = true; 350 this.lastPaintKey = ''; 351 this.paintSeed = 1; 352 this._onMove = this.handleMouseMove.bind(this); 353 this._onDown = this.handleMouseDown.bind(this); 354 this._onUp = this.handleMouseUp.bind(this); 355 this._onSetting = this.handleSettingChange.bind(this); 356 357 document.addEventListener('mousemove', this._onMove, { passive: true }); 358 document.addEventListener('mousedown', this._onDown, { passive: true }); 359 document.addEventListener('mouseup', this._onUp, { passive: true }); 360 document.addEventListener('pupwife:setting-changed', this._onSetting); 361 362 this.isPointer = findInteractiveAt(this.mouseX, this.mouseY); 363 this.applyTransform(); 364 this.draw(true); 365 } 366 367 RoughSketchCursor.prototype.getSmudgeLevel = function () { 368 if (this.cursorTypeKey !== 'rough-pencil') return 0; 369 if (this.speed < 5) return 0; 370 if (this.speed < 10) return 1; 371 if (this.speed < 18) return 2; 372 return 3; 373 }; 374 375 RoughSketchCursor.prototype.getPaintKey = function () { 376 return [ 377 this.isPointer ? '1' : '0', 378 this.isClicking ? '1' : '0', 379 isDarkMode() ? '1' : '0',
380 this.getSmudgeLevel() 381 ].join('|'); 382 }; 383 384 RoughSketchCursor.prototype.getInkOpts = function () { 385 return getInkOptions(this.paintSeed); 386 }; 387 388 RoughSketchCursor.prototype.syncPaintState = function (force) { 389 var key = this.getPaintKey(); 390 var keyChanged = key !== this.lastPaintKey; 391 392 if (!force && !this.dirty && !keyChanged) { 393 return false; 394 } 395 396 this.lastPaintKey = key; 397 this.smudgeLevel = this.getSmudgeLevel(); 398 this.paintSeed = paintSeed(this.cursorTypeKey, key); 399 this.dirty = false; 400 401 var roughApi = getRoughApi(); 402 if (roughApi) { 403 this.rc = roughApi.canvas(this.canvas, { seed: this.paintSeed }); 404 } 405 406 return true; 407 }; 408 409 RoughSketchCursor.prototype.markDirty = function () { 410 this.dirty = true; 411 }; 412 413 RoughSketchCursor.prototype.handleMouseMove = function (e) { 414 var dx = e.clientX - this.mouseX; 415 var dy = e.clientY - this.mouseY; 416 this.speed = Math.sqrt(dx * dx + dy * dy); 417 global.__pupwifeLastMouseX = e.clientX; 418 global.__pupwifeLastMouseY = e.clientY; 419 this.mouseX = e.clientX; 420 this.mouseY = e.clientY; 421 422 var nextPointer = findInteractiveAt(e.clientX, e.clientY); 423 if (nextPointer !== this.isPointer) { 424 this.isPointer = nextPointer; 425 this.markDirty(); 426 } else if (this.cursorTypeKey === 'rough-pencil') { 427 var nextSmudge = this.getSmudgeLevel(); 428 if (nextSmudge !== this.smudgeLevel) { 429 this.markDirty(); 430 } 431 } 432 }; 433 434 RoughSketchCursor.prototype.handleMouseDown = function () { 435 if (!this.isClicking) { 436 this.isClicking = true; 437 this.markDirty(); 438 } 439 }; 440 441 RoughSketchCursor.prototype.handleMouseUp = function () { 442 if (this.isClicking) { 443 this.isClicking = false; 444 this.markDirty(); 445 } 446 }; 447 448 RoughSketchCursor.prototype.handleSettingChange = function () { 449 this.markDirty(); 450 this.draw(true); 451 }; 452 453 RoughSketchCursor.prototype.applyTransform = function () { 454 var scale = this.isClicking ? (this.spec.clickScale || 0.9) : 1; 455 var left = this.mouseX - this.spec.hotspotX; 456 var top = this.mouseY - this.spec.hotspotY; 457 this.canvas.style.transform = 458 'translate3d(' + left + 'px,' + top + 'px,0) scale(' + scale + ')'; 459 }; 460 461 RoughSketchCursor.prototype.draw = function (force) { 462 if (!this.rc || !this.spec.draw) return; 463 if (!this.syncPaintState(!!force)) return; 464 465 var ctx = this.canvas.getContext('2d'); 466 if (!ctx) return; 467 468 ctx.clearRect(0, 0, this.spec.canvasW, this.spec.canvasH); 469 this.spec.draw(this); 470 }; 471 472 RoughSketchCursor.prototype.update = function () { 473 this.applyTransform(); 474 this.draw(false); 475 this.speed *= 0.78; 476 }; 477 478 RoughSketchCursor.prototype.destroy = function () { 479 document.removeEventListener('mousemove', this._onMove); 480 document.removeEventListener('mousedown', this._onDown); 481 document.removeEventListener('mouseup', this._onUp); 482 document.removeEventListener('pupwife:setting-changed', this._onSetting); 483 }; 484 485 function getRoughApi() { 486 if (global.rough && typeof global.rough.canvas === 'function') return global.rough; 487 return null; 488 } 489 490 function findRoughLibScript() { 491 return document.querySelector('script[' + ROUGH_LIB_MARK + '], script[src*="bundled/rough.js"]'); 492 } 493 494 function ensureRoughJs() { 495 var existing = getRoughApi(); 496 if (existing) return Promise.resolve(existing); 497 498 if (roughLoadPromise) { 499 return roughLoadPromise.then(function (api) { 500 if (api) return api; 501 roughLoadPromise = null; 502 return ensureRoughJs(); 503 }); 504 } 505 506 roughLoadPromise = new Promise(function (resolve) { 507 var tag = findRoughLibScript(); 508 if (tag) { 509 if (getRoughApi()) { 510 resolve(getRoughApi()); 511 return; 512 } 513 var done = function () { resolve(getRoughApi()); }; 514 tag.addEventListener('load', done, { once: true }); 515 tag.addEventListener('error', done, { once: true }); 516 global.setTimeout(function () { resolve(getRoughApi()); }, 100); 517 return; 518 } 519 520 var script = document.createElement('script'); 521 script.src = ROUGH_JS_SRC; 522 script.setAttribute(ROUGH_LIB_MARK, '1'); 523 script.async = false;
524 script.onload = function () { resolve(getRoughApi()); }; 525 script.onerror = function () { 526 emitDevWarn('Rough.js failed to load â sketch cursors unavailable.'); 527 resolve(null); 528 }; 529 document.head.appendChild(script); 530 }); 531 532 return roughLoadPromise; 533 } 534 535 function ensureStyles() { 536 if (document.getElementById('roughjs-cursor-style')) return; 537 var style = document.createElement('style'); 538 style.id = 'roughjs-cursor-style'; 539 style.textContent = 540 'html.roughjs-cursor-active, html.roughjs-cursor-active *, ' + 541 'body.roughjs-cursor-active, body.roughjs-cursor-active * { cursor: none !important; }' + 542 '#roughjs-sketch-cursor { pointer-events: none !important; touch-action: none !important; }'; 543 document.head.appendChild(style); 544 } 545 546 function trackLastMouse(e) { 547 global.__pupwifeLastMouseX = e.clientX; 548 global.__pupwifeLastMouseY = e.clientY; 549 } 550 551 function ensureCanvas(spec) { 552 if (!canvas) { 553 canvas = document.createElement('canvas'); 554 canvas.id = 'roughjs-sketch-cursor'; 555 canvas.setAttribute('aria-hidden', 'true'); 556 canvas.style.cssText = 557 'position:fixed;top:0;left:0;pointer-events:none!important;will-change:transform;z-index:2147483000;'; 558 document.body.appendChild(canvas); 559 } 560 561 canvas.width = spec.canvasW; 562 canvas.height = spec.canvasH; 563 canvas.style.width = spec.canvasW + 'px'; 564 canvas.style.height = spec.canvasH + 'px'; 565 canvas.style.transformOrigin = spec.hotspotX + 'px ' + spec.hotspotY + 'px'; 566 567 return canvas; 568 } 569 570 function setCursorActive(active) { 571 document.documentElement.classList.toggle('roughjs-cursor-active', active); 572 document.body.classList.toggle('roughjs-cursor-active', active); 573 } 574 575 function stopLoop() { 576 if (animId) { 577 cancelAnimationFrame(animId); 578 animId = null; 579 } 580 } 581 582 function startLoop() { 583 stopLoop(); 584 function tick() { 585 if (currentCursor && typeof currentCursor.update === 'function') { 586 currentCursor.update(); 587 } 588 animId = requestAnimationFrame(tick); 589 } 590 animId = requestAnimationFrame(tick); 591 } 592 593 function teardownCursor() { 594 stopLoop(); 595 if (currentCursor && typeof currentCursor.destroy === 'function') { 596 currentCursor.destroy(); 597 } 598 currentCursor = null; 599 currentSpec = null; 600 roughCanvas = null; 601 if (canvas) { 602 var ctx = canvas.getContext('2d'); 603 if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height); 604 if (canvas.parentNode) canvas.parentNode.removeChild(canvas); 605 canvas = null; 606 } 607 setCursorActive(false); 608 } 609 610 function activateRoughCursor(cursorType) { 611 var spec = CURSOR_SPECS[cursorType]; 612 var roughApi = getRoughApi(); 613 if (!spec || !roughApi) { 614 emitDevWarn('Rough.js cursor unavailable: ' + cursorType); 615 return false; 616 } 617 618 ensureStyles(); 619 document.addEventListener('mousemove', trackLastMouse, { passive: true }); 620 621 var targetCanvas = ensureCanvas(spec); 622 roughCanvas = roughApi.canvas(targetCanvas); 623 currentSpec = spec; 624 currentCursor = new RoughSketchCursor(spec, targetCanvas, roughCanvas, cursorType); 625 setCursorActive(true); 626 startLoop(); 627 emitDevLog('Rough.js cursor set to: ' + cursorType); 628 return true; 629 } 630 631 function setCursor(cursorType) { 632 var nextType = cursorType || 'none'; 633 634 if (nextType === currentCursorType && currentCursor && getRoughApi()) { 635 return Promise.resolve(); 636 } 637 638 pendingCursorType = nextType; 639 teardownCursor(); 640 currentCursorType = pendingCursorType; 641 642 if (!cursorType || cursorType === 'none') { 643 pendingCursorType = null; 644 return Promise.resolve(); 645 } 646 647 if (!CURSOR_SPECS[cursorType]) { 648 emitDevWarn('Unknown Rough.js cursor type: ' + cursorType); 649 pendingCursorType = null; 650 return Promise.resolve(); 651 } 652 653 return ensureRoughJs() 654 .then(function (roughApi) { 655 if (!roughApi) {
656 console.warn('[Sketch cursor] Rough.js did not load. Check network or ad blockers.'); 657 pendingCursorType = null; 658 return; 659 } 660 if (pendingCursorType !== cursorType) return; 661 if (!activateRoughCursor(cursorType)) { 662 console.warn('[Sketch cursor] Failed to start: ' + cursorType); 663 pendingCursorType = null; 664 } 665 }) 666 .catch(function (err) { 667 pendingCursorType = null; 668 console.error('Failed to activate Rough.js cursor:', err); 669 }); 670 } 671 672 function getCursor() { 673 return currentCursorType; 674 } 675 676 function cleanup() { 677 teardownCursor(); 678 document.removeEventListener('mousemove', trackLastMouse); 679 var style = document.getElementById('roughjs-cursor-style'); 680 if (style) style.remove(); 681 } 682 683 function readSavedRoughCursor() { 684 try { 685 var raw = global.localStorage && global.localStorage.getItem('puppywifey_settings'); 686 if (!raw) return null; 687 var parsed = JSON.parse(raw); 688 if (parsed.customCursor && String(parsed.customCursor).indexOf('rough-') === 0) { 689 return parsed.customCursor; 690 } 691 } catch (e) { /* ignore */ } 692 return null; 693 } 694 695 global.RoughJSCursor = { 696 setCursor: setCursor, 697 getCursor: getCursor, 698 cleanup: cleanup, 699 CURSOR_TYPES: CURSOR_TYPES, 700 SPECS: CURSOR_SPECS 701 }; 702 703 document.addEventListener('pupwife:apply-custom-cursor', function () { 704 var saved = readSavedRoughCursor(); 705 if (saved) setCursor(saved); 706 }); 707})(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.