PageSourceSearch

https://ole.ee/wp-content/plugins/scratch-reveal/assets/js/scratch-reveal.js?ver=0.9.5

js ole.ee collected 2026-10-02 22:39:09 UTC 40,520 bytes, 1,123 lines download raw bytes

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.