1(function () { 2 'use strict'; 3 4 document.addEventListener('DOMContentLoaded', function () { 5 var containers = document.querySelectorAll('.scratch-reveal-container'); 6 containers.forEach(function (container) { 7 setupScratchReveal(container); 8 }); 9 }); 10 11 // --- Lightbox (zoom + drag + X) --- 12 function openSrLightbox(url, styleConfig) { 13 if (!url) return; 14 styleConfig = styleConfig || {}; 15 16 var existing = document.querySelector('.sr-lightbox-backdrop'); 17 if (existing && existing.parentNode) { 18 existing.parentNode.removeChild(existing); 19 } 20 21 var backdrop = document.createElement('div'); 22 backdrop.className = 'sr-lightbox-backdrop'; 23 24 var inner = document.createElement('div'); 25 inner.className = 'sr-lightbox-inner'; 26 inner.style.position = 'relative'; 27 inner.style.display = 'flex'; 28 inner.style.alignItems = 'center'; 29 inner.style.justifyContent = 'center'; 30 31 var img = document.createElement('img'); 32 img.className = 'sr-lightbox-image'; 33 img.src = url; 34 img.alt = ''; 35 img.style.transformOrigin = 'center center'; 36 37 inner.appendChild(img); 38 backdrop.appendChild(inner); 39 40 // Close button (X) 41 var closeBtn = document.createElement('button'); 42 closeBtn.type = 'button'; 43 closeBtn.className = 'sr-lightbox-close'; 44 closeBtn.textContent = 'Ã'; 45 46 var normalTextColor = styleConfig.textColor || styleConfig.lightboxTextColor || ''; 47 var normalBgColor = styleConfig.bgColor || styleConfig.lightboxBgColor || ''; 48 var hoverTextColor = styleConfig.textColorHover || styleConfig.lightboxTextColorHover || ''; 49 var hoverBgColor = styleConfig.bgColorHover || styleConfig.lightboxBgColorHover || ''; 50 51 if (styleConfig.fontFamily) { 52 closeBtn.style.fontFamily = styleConfig.fontFamily + ', sans-serif'; 53 } 54 if (styleConfig.fontSize) { 55 closeBtn.style.fontSize = styleConfig.fontSize; 56 } 57 if (normalTextColor) { 58 closeBtn.style.color = normalTextColor; 59 } 60 if (normalBgColor) { 61 closeBtn.style.background = normalBgColor; 62 } 63 if (styleConfig.borderRadius) { 64 closeBtn.style.borderRadius = styleConfig.borderRadius; 65 } 66 67 closeBtn.addEventListener('mouseenter', function () { 68 if (hoverTextColor) { 69 closeBtn.style.color = hoverTextColor; 70 } 71 if (hoverBgColor) { 72 closeBtn.style.background = hoverBgColor; 73 } 74 }); 75 76 closeBtn.addEventListener('mouseleave', function () { 77 if (normalTextColor) { 78 closeBtn.style.color = normalTextColor; 79 } 80 if (normalBgColor) { 81 closeBtn.style.background = normalBgColor; 82 } 83 }); 84 85 backdrop.appendChild(closeBtn); 86 document.body.appendChild(backdrop); 87 88 var zoomed = false; 89 var zoomScale = 1; 90 var offsetX = 0; 91 var offsetY = 0; 92 var isPanning = false; 93 var startX = 0; 94 var startY = 0; 95 var startOffsetX = 0; 96 var startOffsetY = 0; 97 var dragged = false; 98 var lastTouchTime = 0; 99 100 function updateTransform() { 101 var scale = zoomed ? zoomScale : 1; 102 var transform = 'translate3d(' + offsetX + 'px,' + offsetY + 'px,0) scale(' + scale + ')'; 103 img.style.transform = transform; 104 } 105 106 function setZoom(state) { 107 zoomed = state; 108 if (zoomed) { 109 backdrop.classList.add('sr-lightbox-zoomed'); 110 img.classList.add('sr-lightbox-image-zoomed'); 111 } else { 112 backdrop.classList.remove('sr-lightbox-zoomed'); 113 img.classList.remove('sr-lightbox-image-zoomed'); 114 offsetX = 0; 115 offsetY = 0; 116 } 117 updateTransform(); 118 } 119 120 // --- UUS: arvesta devicePixelRatio, et mitte zoomida üle "natural size" --- 121 function computeZoomScale() { 122 var rect = img.getBoundingClientRect();
123 if (!rect.width || !rect.height || !img.naturalWidth || !img.naturalHeight) { 124 zoomScale = 2; 125 return; 126 } 127 128 var dpr = window.devicePixelRatio || 1; 129 130 // natural CSS-laius = naturalWidth / DPR 131 var naturalCssWidth = img.naturalWidth / dpr; 132 var naturalCssHeight = img.naturalHeight / dpr; 133 134 var scaleX = naturalCssWidth / rect.width; 135 var scaleY = naturalCssHeight / rect.height; 136 137 // võtame potentiaalselt suurema, et vähemalt üks mõõt jõuaks natural-size'ini 138 var maxScale = Math.max(scaleX, scaleY); 139 140 if (!isFinite(maxScale) || maxScale < 1) { 141 maxScale = 1; // ära up-scale'i kui pilt on juba ~natural-size 142 } 143 144 zoomScale = maxScale; 145 } 146 147 function toggleZoom() { 148 if (!zoomed) { 149 computeZoomScale(); 150 setZoom(true); 151 } else { 152 setZoom(false); 153 } 154 } 155 156 img.addEventListener('load', function () { 157 updateTransform(); 158 }); 159 160 // Mouse pan 161 function onMouseDown(e) { 162 if (!zoomed) return; 163 isPanning = true; 164 dragged = false; 165 startX = e.clientX; 166 startY = e.clientY; 167 startOffsetX = offsetX; 168 startOffsetY = offsetY; 169 e.preventDefault(); 170 } 171 172 function onMouseMove(e) { 173 if (!isPanning) return; 174 var dx = e.clientX - startX; 175 var dy = e.clientY - startY; 176 if (Math.abs(dx) > 3 || Math.abs(dy) > 3) { 177 dragged = true; 178 } 179 offsetX = startOffsetX + dx; 180 offsetY = startOffsetY + dy; 181 updateTransform(); 182 } 183 184 function onMouseUp() { 185 isPanning = false; 186 } 187 188 img.addEventListener('mousedown', onMouseDown); 189 document.addEventListener('mousemove', onMouseMove); 190 document.addEventListener('mouseup', onMouseUp); 191 192 // Touch pan 193 function onTouchStart(e) { 194 if (!zoomed) return; 195 if (!e.touches || !e.touches.length) return; 196 var t = e.touches[0]; 197 isPanning = true; 198 dragged = false; 199 startX = t.clientX; 200 startY = t.clientY; 201 startOffsetX = offsetX; 202 startOffsetY = offsetY; 203 e.preventDefault(); 204 } 205 206 function onTouchMove(e) { 207 if (!isPanning) return; 208 if (!e.touches || !e.touches.length) return; 209 var t = e.touches[0]; 210 var dx = t.clientX - startX; 211 var dy = t.clientY - startY; 212 if (Math.abs(dx) > 3 || Math.abs(dy) > 3) { 213 dragged = true; 214 } 215 offsetX = startOffsetX + dx; 216 offsetY = startOffsetY + dy; 217 updateTransform(); 218 e.preventDefault(); 219 } 220 221 function onTouchEndDocument() { 222 isPanning = false; 223 } 224 225 img.addEventListener('touchstart', onTouchStart, { passive: false }); 226 document.addEventListener('touchmove', onTouchMove, { passive: false }); 227 document.addEventListener('touchend', onTouchEndDocument); 228 229 // Mobiilis: tap (touchend) pildil = zoom in/out 230 img.addEventListener('touchend', function (e) { 231 if (dragged) { 232 dragged = false; 233 return; 234 } 235 lastTouchTime = Date.now(); 236 e.preventDefault(); 237 toggleZoom(); 238 }, { passive: false }); 239 240 // Desktop: click = zoom toggle. Mobiilis ignoreerime ghost-clicki pärast touchi 241 img.addEventListener('click', function (e) { 242 e.stopPropagation(); 243 if (Date.now() - lastTouchTime < 500) { 244 return; 245 } 246 if (dragged) { 247 dragged = false; 248 return; 249 } 250 toggleZoom(); 251 }); 252 253 // Close behaviour 254 function closeLightbox() { 255 document.removeEventListener('keydown', onKey); 256 document.removeEventListener('mousemove', onMouseMove); 257 document.removeEventListener('mouseup', onMouseUp); 258 document.removeEventListener('touchmove', onTouchMove); 259 document.removeEventListener('touchend', onTouchEndDocument); 260 261 if (!backdrop.parentNode) return; 262 backdrop.classList.add('sr-lightbox-hide'); 263 setTimeout(function () { 264 if (backdrop.parentNode) { 265 backdrop.parentNode.removeChild(backdrop); 266 } 267 }, 200); 268 } 269 270 function onKey(e) { 271 if (e.key === 'Escape') { 272 closeLightbox(); 273 } 274 } 275 276 document.addEventListener('keydown', onKey); 277 278 backdrop.addEventListener('click', function (e) { 279 if (e.target === backdrop) { 280 closeLightbox(); 281 } 282 }); 283 284 closeBtn.addEventListener('click', function (e) { 285 e.stopPropagation(); 286 closeLightbox(); 287 }); 288 } 289 290 // --------------------------------------------- 291 // Scratch Reveal põhilogika (muutmata) 292 // --------------------------------------------- 293 function setupScratchReveal(container) { 294 var canvas = container.querySelector('.sr-overlay-canvas'); 295 var content = container.querySelector('.sr-content'); 296 if (!canvas || !content) return; 297 298 var ctx = canvas.getContext('2d'); 299 var dpr = window.devicePixelRatio || 1; 300
301 canvas.style.touchAction = 'none'; 302 303 var overlayMode = container.getAttribute('data-overlay-mode') || 'color'; 304 var overlayColor = container.getAttribute('data-overlay-color') || '#ffffff'; 305 var overlayImage = container.getAttribute('data-overlay-image') || ''; 306 var overlayFit = container.getAttribute('data-overlay-fit') || 'cover'; 307 var coverage = container.getAttribute('data-coverage') || 'full'; 308 var coverageAmount = parseFloat(container.getAttribute('data-coverage-amount') || '0.7'); 309 310 var brushShape = container.getAttribute('data-brush-shape') || 'round'; 311 var brushSize = parseFloat(container.getAttribute('data-brush-size') || '40'); 312 var brushSoftness = parseFloat(container.getAttribute('data-brush-softness') || '0.5'); 313 var brushPressure = parseFloat(container.getAttribute('data-brush-pressure') || '1'); 314 var brushImageUrl = container.getAttribute('data-brush-image') || ''; 315 316 var cursorImageUrl = container.getAttribute('data-cursor-image') || ''; 317 var cursorSize = parseFloat(container.getAttribute('data-cursor-size') || '40'); 318 var cursorMobileOffset = parseFloat(container.getAttribute('data-cursor-mobile-offset') || '0'); 319 var enableCursor = container.getAttribute('data-enable-cursor') === '1'; 320 var cursorWiggleEnabled = container.getAttribute('data-cursor-wiggle') === '1'; 321 322 var hasReset = container.getAttribute('data-has-reset') === '1'; 323 324 var autoRecoverEnabled = container.getAttribute('data-auto-recover-enabled') === '1'; 325 var autoRecoverDelaySec = parseFloat(container.getAttribute('data-auto-recover-delay') || '15'); 326 var autoRecoverSpeedSec = parseFloat(container.getAttribute('data-auto-recover-speed') || '0.5'); 327 328 var showProgress = container.getAttribute('data-show-progress') === '1'; 329 var progressHeight = parseFloat(container.getAttribute('data-progress-height') || '3'); 330 var progressColor = container.getAttribute('data-progress-color') || '#00aa88'; 331 var progressTrackColor = container.getAttribute('data-progress-track-color') || ''; 332 var progressFontSize = container.getAttribute('data-progress-font-size') || ''; 333 var progressFontFamily = container.getAttribute('data-progress-font-family') || ''; 334 var progressFontWeight = container.getAttribute('data-progress-font-weight') || ''; 335 var progressFontStyle = container.getAttribute('data-progress-font-style') || ''; 336 var progressTextColor = container.getAttribute('data-progress-text-color') || ''; 337 var progressBgColor = container.getAttribute('data-progress-bg-color') || ''; 338 339 var mobileLongpressDelay = parseFloat(container.getAttribute('data-mobile-longpress-delay') || '3'); 340 341 var lbFontFamily = container.getAttribute('data-lightbox-font-family') || ''; 342 var lbFontSize = container.getAttribute('data-lightbox-font-size') || '20px'; 343 var lbTextColor = container.getAttribute('data-lightbox-text-color') || '#ffffff'; 344 var lbBgColor = container.getAttribute('data-lightbox-bg-color') || 'rgba(0,0,0,0.7)'; 345 var lbTextColorHover = container.getAttribute('data-lightbox-text-color-hover') || '#ffffff'; 346 var lbBgColorHover = container.getAttribute('data-lightbox-bg-color-hover') || 'rgba(0,0,0,0.9)'; 347 var lbBorderRadius = container.getAttribute('data-lightbox-border-radius') || '50%'; 348 349 var sizeMode = container.getAttribute('data-size-mode') || 'custom'; 350 var aspectRatioStr = container.getAttribute('data-aspect-ratio') || '16:9'; 351 352 var messageWrap = container.querySelector('.sr-message'); 353 var messageInner = container.querySelector('.sr-message-inner'); 354 355 var hintWrapper = container.querySelector('.sr-overlay-hint') || container.querySelector('.sr-overlay-hint-text'); 356 var handHint = container.querySelector('.sr-hand-hint'); 357 var resetBtn = container.querySelector('.sr-reset-btn'); 358 359 var progressBar = container.querySelector('.sr-progress-bar'); 360 var progressInner = progressBar ? progressBar.querySelector('.sr-progress-inner') : null; 361 var progressPercentEl = null; 362 363 var imageLink = container.querySelector('.sr-image-link'); 364 365 var isDrawing = false;
366 var erasedPercent = 0; 367 var autoRecoverTimer = null; 368 var longPressTimer = null; 369 var longPressActivated = false; 370 var longPressStartPos = { x: 0, y: 0 }; 371 var lastProgressCheck = 0; 372 var hasScratched = false; 373 var isRecovering = false; 374 375 var brushImage = null; 376 if (brushShape === 'custom' && brushImageUrl) { 377 brushImage = new Image(); 378 brushImage.crossOrigin = 'anonymous'; 379 brushImage.src = brushImageUrl; 380 } 381 382 var overlayImg = null; 383 if (overlayMode === 'image' && overlayImage) { 384 overlayImg = new Image(); 385 overlayImg.crossOrigin = 'anonymous'; 386 overlayImg.src = overlayImage; 387 } 388 389 var overlayNeedsRedraw = true; 390 391 var brushOffscreenCanvas = null; 392 var brushOffscreenCtx = null; 393 var brushOffscreenSize = 0; 394 395 // progress % tekst 396 if (showProgress && progressBar && progressInner) { 397 progressPercentEl = document.createElement('div'); 398 progressPercentEl.className = 'sr-progress-percent'; 399 progressPercentEl.textContent = '0%'; 400 401 if (progressFontSize) { 402 progressPercentEl.style.fontSize = progressFontSize; 403 } 404 if (progressFontFamily) { 405 progressPercentEl.style.fontFamily = progressFontFamily + ', sans-serif'; 406 } 407 if (progressFontWeight) { 408 progressPercentEl.style.fontWeight = progressFontWeight; 409 } 410 if (progressFontStyle) { 411 progressPercentEl.style.fontStyle = progressFontStyle; 412 } 413 if (progressTextColor) { 414 progressPercentEl.style.color = progressTextColor; 415 } 416 if (progressBgColor !== '') { 417 progressPercentEl.style.background = progressBgColor; 418 } else { 419 progressPercentEl.style.background = 'transparent'; 420 } 421 422 progressBar.appendChild(progressPercentEl); 423 424 if (progressHeight > 0) { 425 progressBar.style.height = progressHeight + 'px'; 426 } 427 if (progressColor) { 428 progressInner.style.background = progressColor; 429 } 430 if (progressTrackColor) { 431 progressBar.style.background = progressTrackColor; 432 } 433 } 434 435 var cursorHalfSize = cursorSize > 0 ? cursorSize / 2 : 20; 436 437 if (enableCursor && handHint) { 438 container.classList.add('sr-cursor-enabled'); 439 440 if (cursorSize > 0) { 441 handHint.style.width = cursorSize + 'px'; 442 handHint.style.height = cursorSize + 'px'; 443 } 444 445 if (cursorImageUrl) { 446 handHint.style.backgroundImage = "url('" + cursorImageUrl + "')"; 447 } 448 449 var hb = handHint.getBoundingClientRect(); 450 if (hb.width > 0) { 451 cursorHalfSize = hb.width / 2; 452 } 453 454 if (cursorWiggleEnabled) { 455 handHint.classList.add('sr-hand-hint-wiggle'); 456 } else { 457 handHint.classList.remove('sr-hand-hint-wiggle'); 458 } 459 } else if (handHint) { 460 container.classList.remove('sr-cursor-enabled'); 461 handHint.classList.add('sr-hidden'); 462 } 463 464 if (resetBtn && handHint) { 465 resetBtn.addEventListener('mouseenter', function () { 466 handHint.classList.add('sr-hidden'); 467 container.classList.remove('sr-cursor-enabled'); 468 }); 469 resetBtn.addEventListener('mouseleave', function () { 470 if (enableCursor) { 471 handHint.classList.remove('sr-hidden'); 472 container.classList.add('sr-cursor-enabled'); 473 } 474 }); 475 } 476 477 if (imageLink) { 478 imageLink.addEventListener('click', function (e) { 479 var isHidden = 480 canvas.style.display === 'none' || 481 canvas.style.visibility === 'hidden' || 482 erasedPercent >= 99.5; 483
484 if (!isHidden) { 485 e.preventDefault(); 486 return; 487 } 488 489 e.preventDefault(); 490 var href = imageLink.getAttribute('href'); 491 492 var lbConfig = { 493 fontFamily: lbFontFamily, 494 fontSize: lbFontSize, 495 textColor: lbTextColor, 496 bgColor: lbBgColor, 497 textColorHover: lbTextColorHover, 498 bgColorHover: lbBgColorHover, 499 borderRadius: lbBorderRadius 500 }; 501 502 openSrLightbox(href, lbConfig); 503 }); 504 } 505 506 function parseAspectRatio(str) { 507 if (!str) return 9 / 16; 508 var parts = String(str).split(':'); 509 if (parts.length !== 2) return 9 / 16; 510 var w = parseFloat(parts[0]) || 16; 511 var h = parseFloat(parts[1]) || 9; 512 if (w <= 0 || h <= 0) return 9 / 16; 513 return h / w; 514 } 515 516 function fitTextToContainer() { 517 if (!messageInner || !messageWrap) return; 518 519 var maxSize = parseFloat(messageInner.getAttribute('data-font-size') || '24'); 520 if (!maxSize || isNaN(maxSize)) { 521 maxSize = 24; 522 } 523 var fontSize = maxSize; 524 var minSize = 8; 525 526 messageInner.style.fontSize = fontSize + 'px'; 527 messageInner.style.whiteSpace = 'pre-wrap'; 528 messageInner.style.display = 'inline-block'; 529 530 var safety = 0; 531 while (safety < 60) { 532 safety++; 533 var wrapHeight = messageWrap.clientHeight; 534 var wrapWidth = messageWrap.clientWidth; 535 var textHeight = messageInner.scrollHeight; 536 var textWidth = messageInner.scrollWidth; 537 538 if ((textHeight <= wrapHeight && textWidth <= wrapWidth) || fontSize <= minSize) { 539 break; 540 } 541 fontSize -= 1; 542 messageInner.style.fontSize = fontSize + 'px'; 543 } 544 } 545 546 function positionInitialCursor() { 547 if (!enableCursor || !handHint) return; 548 549 var rect = container.getBoundingClientRect(); 550 var padding = 16; 551 var x = rect.width - cursorHalfSize - padding; 552 var y = rect.height - cursorHalfSize - padding; 553 554 handHint.style.left = x + 'px'; 555 handHint.style.top = y + 'px'; 556 handHint.classList.remove('sr-hidden'); 557 container.classList.add('sr-cursor-enabled'); 558 } 559 560 function sizeCanvas() { 561 var width = content.clientWidth || container.clientWidth || 0; 562 var height = 0; 563 564 if (width <= 0) { 565 return; 566 } 567 568 if (sizeMode === 'ratio') { 569 var ratio = parseAspectRatio(aspectRatioStr); 570 height = width * ratio; 571 container.style.height = height + 'px'; 572 container.classList.add('sr-size-ratio'); 573 } else { 574 height = content.clientHeight || container.clientHeight || 0; 575 if (height <= 0) { 576 height = 300; 577 container.style.height = height + 'px'; 578 } 579 container.classList.remove('sr-size-ratio'); 580 } 581 582 canvas.width = width * dpr; 583 canvas.height = height * dpr; 584 canvas.style.width = width + 'px'; 585 canvas.style.height = height + 'px'; 586 canvas.style.opacity = '1'; 587 canvas.style.transition = 'opacity 0.4s ease'; 588 589 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 590 overlayNeedsRedraw = true; 591 drawOverlay(); 592 593 requestAnimationFrame(function () { 594 fitTextToContainer(); 595 positionInitialCursor(); 596 }); 597 } 598 599 function paintOverlay(clearFirst, alphaOverride) { 600 var width = canvas.width / dpr; 601 var height = canvas.height / dpr; 602 if (width <= 0 || height <= 0) return; 603 604 if (clearFirst) { 605 ctx.clearRect(0, 0, width, height); 606 } 607 608 var baseAlpha = (coverage === 'partial') ? coverageAmou
608nt : 1; 609 var alpha = (typeof alphaOverride === 'number') ? alphaOverride : baseAlpha; 610 611 ctx.save(); 612 ctx.globalAlpha = alpha; 613 614 if (overlayMode === 'color' || !overlayImg) { 615 ctx.fillStyle = overlayColor || '#ffffff'; 616 ctx.fillRect(0, 0, width, height); 617 } else { 618 var iw = overlayImg.naturalWidth; 619 var ih = overlayImg.naturalHeight; 620 if (iw && ih) { 621 var scale; 622 if (overlayFit === 'contain') { 623 scale = Math.min(width / iw, height / ih); 624 } else { 625 scale = Math.max(width / iw, height / ih); 626 } 627 var dw = iw * scale; 628 var dh = ih * scale; 629 var dx = (width - dw) / 2; 630 var dy = (height - dh) / 2; 631 ctx.drawImage(overlayImg, dx, dy, dw, dh); 632 } else { 633 ctx.fillStyle = overlayColor || '#ffffff'; 634 ctx.fillRect(0, 0, width, height); 635 } 636 } 637 638 ctx.restore(); 639 } 640 641 function drawOverlay() { 642 if (!overlayNeedsRedraw) return; 643 overlayNeedsRedraw = false; 644 645 paintOverlay(true, null); 646 647 erasedPercent = 0; 648 hasScratched = false; 649 updateProgress(0); 650 651 if (hintWrapper) { 652 hintWrapper.classList.remove('sr-hidden'); 653 } 654 655 canvas.style.display = 'block'; 656 canvas.style.opacity = '1'; 657 658 if (enableCursor && handHint) { 659 container.classList.add('sr-cursor-enabled'); 660 handHint.classList.remove('sr-hidden'); 661 } 662 663 if (progressBar) { 664 progressBar.classList.remove('sr-hidden'); 665 } 666 } 667 668 function applyCustomBrush(x, y, size, pressure) { 669 if (!brushImage) { 670 ctx.beginPath(); 671 ctx.arc(x, y, size / 2, 0, Math.PI * 2); 672 ctx.fillStyle = 'rgba(0,0,0,' + pressure + ')'; 673 ctx.fill(); 674 return; 675 } 676 677 if (!brushOffscreenCanvas || brushOffscreenSize !== size) { 678 brushOffscreenSize = size; 679 brushOffscreenCanvas = document.createElement('canvas'); 680 brushOffscreenCanvas.width = size; 681 brushOffscreenCanvas.height = size; 682 brushOffscreenCtx = brushOffscreenCanvas.getContext('2d'); 683 } else { 684 brushOffscreenCtx.clearRect(0, 0, brushOffscreenSize, brushOffscreenSize); 685 } 686 687 brushOffscreenCtx.drawImage(brushImage, 0, 0, size, size); 688 var imgData = brushOffscreenCtx.getImageData(0, 0, size, size); 689 var data = imgData.data; 690 691 for (var i = 0; i < data.length; i += 4) { 692 var r = data[i]; 693 var g = data[i + 1]; 694 var b = data[i + 2]; 695 var a = data[i + 3]; 696 697 if (a === 0) continue; 698 699 var brightness = (r + g + b) / 3; 700 var alphaFactor = (255 - brightness) / 255; 701 var newAlpha = a * alphaFactor * pressure; 702 data[i + 3] = newAlpha; 703 } 704 705 brushOffscreenCtx.putImageData(imgData, 0, 0); 706 707 var radius = size / 2; 708 var bx = x - radius; 709 var by = y - radius; 710 711 ctx.drawImage(brushOffscreenCanvas, bx, by); 712 } 713 714 function scratchAt(clientX, clientY, pointerType) { 715 var rect = canvas.getBoundingClientRect(); 716 var x = clientX - rect.left; 717 var y = clientY - rect.top; 718 719 if (x < 0 || y < 0 || x > rect.width || y > rect.height) return; 720 721 if (hintWrapper) { 722 hintWrapper.classList.add('sr-hidden'); 723 } 724 725 hasScratched = true; 726 727 ctx.save(); 728 ctx.globalCompositeOperation = 'destination-out'; 729 730 var size = brushSize; 731 if (pointerType === 'touch') { 732 size *= 1.1; 733 } 734 var radius = size / 2; 735 var softness = Math.max(0, Math.min(1, brushSoftness)); 736 var pressure = Math.max(0, Math.min(1, brushPressure)); 737 738 if (brushShape === 'round') {
739 var innerR = radius * (1 - softness); 740 var grad = ctx.createRadialGradient(x, y, innerR, x, y, radius); 741 grad.addColorStop(0, 'rgba(0,0,0,' + pressure + ')'); 742 grad.addColorStop(1, 'rgba(0,0,0,0)'); 743 ctx.fillStyle = grad; 744 ctx.beginPath(); 745 ctx.arc(x, y, radius, 0, Math.PI * 2); 746 ctx.fill(); 747 } else if (brushShape === 'square') { 748 var half = radius; 749 var g2 = ctx.createLinearGradient(x - half, y - half, x + half, y + half); 750 g2.addColorStop(0, 'rgba(0,0,0,' + pressure + ')'); 751 g2.addColorStop(1, 'rgba(0,0,0,0)'); 752 ctx.fillStyle = g2; 753 ctx.fillRect(x - half, y - half, size, size); 754 } else if (brushShape === 'custom') { 755 applyCustomBrush(x, y, size, pressure); 756 } else { 757 ctx.beginPath(); 758 ctx.arc(x, y, radius, 0, Math.PI * 2); 759 ctx.fillStyle = 'rgba(0,0,0,' + pressure + ')'; 760 ctx.fill(); 761 } 762 763 ctx.restore(); 764 765 var now = Date.now(); 766 if (now - lastProgressCheck > 120) { 767 lastProgressCheck = now; 768 computeProgress(); 769 } 770 771 if (autoRecoverTimer) { 772 clearTimeout(autoRecoverTimer); 773 autoRecoverTimer = null; 774 } 775 } 776 777 function computeProgress() { 778 if (!showProgress || !progressBar || !progressInner) return; 779 780 var w = canvas.width; 781 var h = canvas.height; 782 if (w <= 0 || h <= 0) return; 783 784 var step = 8; 785 786 try { 787 var imageData = ctx.getImageData(0, 0, w, h); 788 var data = imageData.data; 789 var total = 0; 790 var erased = 0; 791 792 for (var i = 3; i < data.length; i += 4 * step) { 793 total++; 794 if (data[i] < 10) { 795 erased++; 796 } 797 } 798 799 var percent = total > 0 ? (erased / total) * 100 : 0; 800 percent = Math.min(100, Math.max(0, percent)); 801 erasedPercent = percent; 802 updateProgress(erasedPercent); 803 804 if (erasedPercent >= 99.5) { 805 erasedPercent = 100; 806 updateProgress(100); 807 808 canvas.style.display = 'none'; 809 810 if (hintWrapper) { 811 hintWrapper.classList.add('sr-hidden'); 812 } 813 if (enableCursor && handHint) { 814 handHint.classList.add('sr-hidden'); 815 container.classList.remove('sr-cursor-enabled'); 816 } 817 if (progressBar) { 818 progressBar.classList.add('sr-hidden'); 819 } 820 } else { 821 if (progressBar) { 822 progressBar.classList.remove('sr-hidden'); 823 } 824 } 825 826 } catch (e) { 827 // ignore 828 } 829 } 830 831 function updateProgress(percent) { 832 if (!showProgress || !progressBar || !progressInner) return; 833 834 var clamped = Math.max(0, Math.min(100, percent)); 835 progressInner.style.width = clamped + '%'; 836 837 if (progressPercentEl) { 838 progressPercentEl.textContent = Math.round(clamped) + '%'; 839 } 840 841 if (clamped >= 100) { 842 if (progressBar) { 843 progressBar.classList.add('sr-hidden'); 844 } 845 } else { 846 if (progressBar) { 847 progressBar.classList.remove('sr-hidden'); 848 } 849 } 850 } 851 852 function doAutoRecoverFade() { 853 if (!autoRecoverEnabled) return; 854 if (erasedPercent <= 0.1) return; 855 856 if (autoRecoverTimer) { 857 clearTimeout(autoRecoverTimer); 858 autoRecoverTimer = null; 859 } 860 861 var width = canvas.width / dpr; 862 var height = canvas.height / dpr; 863 if (width <= 0 || height <= 0) return; 864 865 isRecovering = true; 866 867 var recoverCanvas = document.createElement('canvas'); 868 recoverCanvas.className = 'sr-recover-canvas'; 869 recoverCanvas.width = canvas.width; 870 recoverCanvas.height = canvas.height; 871 recoverCanvas.style.opacity = '0'; 872 recoverCanvas.style.transition = 'opacity ' + autoRecoverSpeedSec + 's ease'; 873 874 var rCtx = recoverCanvas.getContext('2d');
875 rCtx.setTransform(dpr, 0, 0, dpr, 0, 0); 876 877 var baseAlpha = (coverage === 'partial') ? coverageAmount : 1; 878 rCtx.save(); 879 rCtx.globalAlpha = baseAlpha; 880 881 if (overlayMode === 'color' || !overlayImg) { 882 rCtx.fillStyle = overlayColor || '#ffffff'; 883 rCtx.fillRect(0, 0, width, height); 884 } else { 885 var iw = overlayImg.naturalWidth; 886 var ih = overlayImg.naturalHeight; 887 if (iw && ih) { 888 var scale; 889 if (overlayFit === 'contain') { 890 scale = Math.min(width / iw, height / ih); 891 } else { 892 scale = Math.max(width / iw, height / ih); 893 } 894 var dw = iw * scale; 895 var dh = ih * scale; 896 var dx = (width - dw) / 2; 897 var dy = (height - dh) / 2; 898 rCtx.drawImage(overlayImg, dx, dy, dw, dh); 899 } else { 900 rCtx.fillStyle = overlayColor || '#ffffff'; 901 rCtx.fillRect(0, 0, width, height); 902 } 903 } 904 rCtx.restore(); 905 906 container.appendChild(recoverCanvas); 907 908 void recoverCanvas.offsetWidth; 909 recoverCanvas.style.opacity = '1'; 910 911 recoverCanvas.addEventListener('transitionend', function onEnd(ev) { 912 if (ev.propertyName !== 'opacity') return; 913 recoverCanvas.removeEventListener('transitionend', onEnd); 914 915 paintOverlay(true, null); 916 917 if (recoverCanvas.parentNode) { 918 recoverCanvas.parentNode.removeChild(recoverCanvas); 919 } 920 921 erasedPercent = 0; 922 updateProgress(0); 923 hasScratched = false; 924 isRecovering = false; 925 926 if (hintWrapper) { 927 hintWrapper.classList.remove('sr-hidden'); 928 } 929 if (progressBar) { 930 progressBar.classList.remove('sr-hidden'); 931 } 932 }); 933 } 934 935 function scheduleAutoRecover() { 936 if (!autoRecoverEnabled) return; 937 938 if (autoRecoverTimer) { 939 clearTimeout(autoRecoverTimer); 940 } 941 942 var isCanvasHidden = 943 canvas.style.display === 'none' || 944 canvas.style.visibility === 'hidden'; 945 946 if (erasedPercent >= 100 || isCanvasHidden) { 947 return; 948 } 949 950 var delaySec = parseFloat(autoRecoverDelaySec); 951 if (isNaN(delaySec) || delaySec <= 0) { 952 delaySec = 15; 953 } 954 var delayMs = delaySec * 1000; 955 956 autoRecoverTimer = setTimeout(function () { 957 doAutoRecoverFade(); 958 }, delayMs); 959 } 960 961 if (hasReset && resetBtn) { 962 resetBtn.addEventListener('click', function (e) { 963 e.preventDefault(); 964 if (autoRecoverTimer) { 965 clearTimeout(autoRecoverTimer); 966 autoRecoverTimer = null; 967 } 968 isRecovering = false; 969 canvas.style.display = 'block'; 970 canvas.style.opacity = '1'; 971 overlayNeedsRedraw = true; 972 drawOverlay(); 973 erasedPercent = 0; 974 updateProgress(0); 975 }); 976 } 977 978 function onPointerDown(e) { 979 var isTouch = e.pointerType === 'touch'; 980 981 if (isTouch && mobileLongpressDelay > 0) { 982 longPressActivated = false; 983 longPressStartPos = { x: e.clientX, y: e.clientY }; 984 longPressTimer = setTimeout(function () { 985 longPressActivated = true; 986 isDrawing = true; 987 container.classList.add('sr-touch-active'); 988 scratchAt(e.clientX, e.clientY, 'touch'); 989 }, mobileLongpressDelay * 1000); 990 } else { 991 isDrawing = true; 992 scratchAt(e.clientX, e.clientY, isTouch ? 'touch' : 'mouse'); 993 } 994 995 if (isTouch) { 996 e.preventDefault(); 997 } 998 999 if (canvas.setPointerCapture) { 1000 canvas.setPointerCapture(e.pointerId); 1001 } 1002 1003 if (enableCursor && handHint) { 1004 handHint.classList.remove('sr-hidden'); 1005 } 1006 } 1007 1008 function onPointerMove(e) { 1009 if (enableCursor && handHint) { 1010 var rect = container.getBoundingClientRect(); 1011 var x = e.clientX - rect.left; 1012 var y = e.clientY - rect.top; 1013 1014 if (e.pointerType === 'touch') { 1015 y -= cursorMobileOffset; 1016 } 1017 1018 handHint.style.left = (x - cursorHalfSize) + 'px'; 1019 handHint.style.top = (y - cursorHalfSize) + 'px'; 1020 } 1021 1022 var isTouch = e.pointerType === 'touch'; 1023 1024 if (!isDrawing) { 1025 if (isTouch && longPressTimer && !longPressActivated) { 1026 var dx = e.clientX - longPressStartPos.x; 1027 var dy = e.clientY - longPressStartPos.y; 1028 var dist2 = dx * dx + dy * dy; 1029 if (dist2 > 25) { 1030 clearTimeout(longPressTimer); 1031 longPressTimer = null; 1032 } 1033 } 1034 return; 1035 } 1036 1037 if (isTouch && !longPressActivated && mobileLongpressDelay > 0) { 1038 return; 1039 } 1040 1041 if (isTouch) { 1042 e.preventDefault(); 1043 } 1044 1045 scratchAt(e.clientX, e.clientY, isTouch ? 'touch' : 'mouse'); 1046 } 1047 1048 function onPointerUp(e) { 1049 isDrawing = false;
1050 container.classList.remove('sr-touch-active'); 1051 1052 if (longPressTimer) { 1053 clearTimeout(longPressTimer); 1054 longPressTimer = null; 1055 } 1056 1057 if (showProgress) { 1058 computeProgress(); 1059 } 1060 1061 scheduleAutoRecover(); 1062 } 1063 1064 function onPointerLeave(e) { 1065 isDrawing = false; 1066 container.classList.remove('sr-touch-active'); 1067 1068 if (longPressTimer) { 1069 clearTimeout(longPressTimer); 1070 longPressTimer = null; 1071 } 1072 1073 if (enableCursor && handHint) { 1074 handHint.style.top = '-9999px'; 1075 handHint.style.left = '-9999px'; 1076 } 1077 1078 scheduleAutoRecover(); 1079 } 1080 1081 canvas.addEventListener('pointerdown', onPointerDown); 1082 canvas.addEventListener('pointermove', onPointerMove); 1083 canvas.addEventListener('pointerup', onPointerUp); 1084 canvas.addEventListener('pointercancel', onPointerUp); 1085 canvas.addEventListener('pointerleave', onPointerLeave); 1086 1087 container.addEventListener('pointermove', function (e) { 1088 if (!enableCursor || !handHint) return; 1089 var rect = container.getBoundingClientRect(); 1090 var x = e.clientX - rect.left; 1091 var y = e.clientY - rect.top; 1092 if (e.pointerType === 'touch') { 1093 y -= cursorMobileOffset; 1094 } 1095 handHint.style.left = (x - cursorHalfSize) + 'px'; 1096 handHint.style.top = (y - cursorHalfSize) + 'px'; 1097 }); 1098 1099 window.addEventListener('resize', function () { 1100 // Enne esimest kraapimist võime vabalt canvast uuesti suurendada/joonistada 1101 if (!hasScratched) { 1102 sizeCanvas(); 1103 } else { 1104 // Pärast kraapimist hoia canvast nii, nagu on â vajadusel ainult sisu tekst ümber 1105 requestAnimationFrame(fitTextToContainer); 1106 } 1107}); 1108 1109 1110 if (overlayImg) { 1111 overlayImg.addEventListener('load', function () { 1112 overlayNeedsRedraw = true; 1113 sizeCanvas(); 1114 }); 1115 } 1116 1117 window.addEventListener('load', function () { 1118 sizeCanvas(); 1119 }); 1120 1121 sizeCanvas(); 1122 } 1123})();
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.