@inappstory/slide-api 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1202,6 +1202,9 @@ class EsModuleSdkApi {
1202
1202
  isSdkSupportCorrectPauseResumeLifecycle() {
1203
1203
  return true;
1204
1204
  }
1205
+ isSdkSupportVerticalSwipeGestureControl() {
1206
+ return true;
1207
+ }
1205
1208
  emitEvent(name, event) {
1206
1209
  this.sdkBinding.onEvent(name, event);
1207
1210
  }
@@ -3469,6 +3472,62 @@ class Reactions {
3469
3472
  static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLElement`, `typeof WidgetReactions.api`, `WidgetCallbacks`, `WidgetDeps`]; }
3470
3473
  }
3471
3474
 
3475
+ class ScratchCard {
3476
+ _elementNodeRef;
3477
+ _widgetApi;
3478
+ _widgetCallbacks;
3479
+ _widgetDeps;
3480
+ static _className = "narrative-element-scratch-card";
3481
+ static className() {
3482
+ return ScratchCard._className;
3483
+ }
3484
+ static isTypeOf(element) {
3485
+ return element instanceof ScratchCard;
3486
+ }
3487
+ constructor(_elementNodeRef, _widgetApi, _widgetCallbacks, _widgetDeps) {
3488
+ this._elementNodeRef = _elementNodeRef;
3489
+ this._widgetApi = _widgetApi;
3490
+ this._widgetCallbacks = _widgetCallbacks;
3491
+ this._widgetDeps = _widgetDeps;
3492
+ const mediaElements = Array.from(this._elementNodeRef.querySelectorAll("img"));
3493
+ this.mediaElementsLoadingPromises = mediaElements.map(waitForImageHtmlElementLoad);
3494
+ }
3495
+ mediaElementsLoadingPromises = [];
3496
+ get nodeRef() {
3497
+ return this._elementNodeRef;
3498
+ }
3499
+ init(localData) {
3500
+ try {
3501
+ this._widgetApi.init(this._elementNodeRef, localData, this._widgetCallbacks, this._widgetDeps);
3502
+ }
3503
+ catch (e) {
3504
+ console.error(e);
3505
+ }
3506
+ return Promise.resolve(true);
3507
+ }
3508
+ onPause() { }
3509
+ onResume() { }
3510
+ onStart() {
3511
+ this._widgetApi.onStart(this._elementNodeRef);
3512
+ }
3513
+ onStop() {
3514
+ this._widgetApi.onStop(this._elementNodeRef);
3515
+ }
3516
+ onBeforeUnmount() {
3517
+ return Promise.resolve();
3518
+ }
3519
+ handleClick() {
3520
+ return false;
3521
+ }
3522
+ get isLayerForcePaused() {
3523
+ return false;
3524
+ }
3525
+ get isClickCaptured() {
3526
+ return this._widgetApi.isClickCaptured(this._elementNodeRef);
3527
+ }
3528
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLElement`, `typeof WidgetScratchCard.api`, `WidgetCallbacks`, `WidgetDeps`]; }
3529
+ }
3530
+
3472
3531
  // export const tryCreateAtLayer = (layerNodeRef: HTMLElement): IElement {
3473
3532
  // const
3474
3533
  // }
@@ -3537,6 +3596,8 @@ const tryCreateFromHtmlElement = (nodeRef, layer, widgetCallbacks, widgetDeps) =
3537
3596
  return layoutApi.widgetTimerApi ? new Timer(nodeRef, layer, layersNodesRefs, layoutApi.widgetTimerApi, widgetCallbacks, widgetDeps) : null;
3538
3597
  case Reactions.className():
3539
3598
  return layoutApi.widgetReactionsApi ? new Reactions(nodeRef, layoutApi.widgetReactionsApi, widgetCallbacks, widgetDeps) : null;
3599
+ case ScratchCard.className():
3600
+ return layoutApi.widgetScratchCardApi ? new ScratchCard(nodeRef, layoutApi.widgetScratchCardApi, widgetCallbacks, widgetDeps) : null;
3540
3601
  }
3541
3602
  }
3542
3603
  return null;
@@ -3744,6 +3805,7 @@ class SlideTimeline {
3744
3805
  if (this.isSDKSupportUpdateTimeline) {
3745
3806
  this.timelineDisabledState = TimelineDisabledState.enabled;
3746
3807
  this.timeSpent = 0;
3808
+ this.resumedAt = new Date().getTime();
3747
3809
  this.updateTimeline("start" /* TIMELINE_ACTION.START */);
3748
3810
  }
3749
3811
  else {
@@ -3988,6 +4050,9 @@ class Layer {
3988
4050
  }
3989
4051
  return null;
3990
4052
  }
4053
+ get scratchCardElements() {
4054
+ return this._elements.filter(element => ScratchCard.isTypeOf(element));
4055
+ }
3991
4056
  get quizElement() {
3992
4057
  for (const element of this._elements) {
3993
4058
  if (Quiz.isTypeOf(element)) {
@@ -5499,10 +5564,10 @@ let SlideApi$1 = class SlideApi {
5499
5564
  return result; // disable all clicks
5500
5565
  }
5501
5566
  }
5502
- if (this.activeLayer.rangeSliderElement) {
5503
- if (this.activeLayer.rangeSliderElement.isClickCapturedBySlider) {
5567
+ if (this.activeLayer.scratchCardElements.length > 0) {
5568
+ if (this.activeLayer.scratchCardElements.some(element => element.isClickCaptured)) {
5504
5569
  result.canClickNext = false;
5505
- return result; // disable all clicks if event captured by slider
5570
+ return result; // disable all clicks if event captured
5506
5571
  }
5507
5572
  }
5508
5573
  if (this.activeLayer.productsElement) {
@@ -5789,6 +5854,9 @@ class SlideApiDepsMultiSlideMode {
5789
5854
  enableVerticalSwipeGesture() {
5790
5855
  this.sdkApi.enableVerticalSwipeGesture();
5791
5856
  }
5857
+ isSdkSupportVerticalSwipeGestureControl() {
5858
+ return this.sdkApi.isSdkSupportVerticalSwipeGestureControl();
5859
+ }
5792
5860
  disableBackpress() {
5793
5861
  this.sdkApi.disableBackpress();
5794
5862
  }
@@ -5963,6 +6031,9 @@ class SlideApiDepsSingleSlideMode {
5963
6031
  enableVerticalSwipeGesture() {
5964
6032
  this.sdkApi.enableVerticalSwipeGesture();
5965
6033
  }
6034
+ isSdkSupportVerticalSwipeGestureControl() {
6035
+ return this.sdkApi.isSdkSupportVerticalSwipeGestureControl();
6036
+ }
5966
6037
  disableBackpress() {
5967
6038
  this.sdkApi.disableBackpress();
5968
6039
  }
@@ -20727,7 +20798,7 @@ class ProductDetailsBottomSheet extends RenderableComponent {
20727
20798
  });
20728
20799
  }
20729
20800
  renderTemplate() {
20730
- return h("div", { class: "ias-products-container-view" }, this.bottomSheet.render());
20801
+ return this.bottomSheet.render();
20731
20802
  }
20732
20803
  open(params) {
20733
20804
  this.showProductDetails(params);
@@ -20805,7 +20876,7 @@ class ProductCheckoutBottomSheet extends RenderableComponent {
20805
20876
  });
20806
20877
  }
20807
20878
  renderTemplate() {
20808
- return h("div", { class: "ias-products-container-view" }, this.bottomSheet.render());
20879
+ return this.bottomSheet.render();
20809
20880
  }
20810
20881
  open() {
20811
20882
  const productCheckout = this.factory.createProductCheckout(this.bottomSheet);
@@ -21582,6 +21653,7 @@ class WidgetProductCarousel extends WidgetBase {
21582
21653
  $carousel = null;
21583
21654
  $track = null;
21584
21655
  isTouchListenersInit = false;
21656
+ productsView = null;
21585
21657
  constructor(element, options, widgetCallbacks, widgetDeps) {
21586
21658
  super(element, options, widgetCallbacks, widgetDeps);
21587
21659
  this.captionView = this.element.querySelector(".narrative-element-text-lines");
@@ -21799,10 +21871,7 @@ class WidgetProductCarousel extends WidgetBase {
21799
21871
  }
21800
21872
  showProductDetails = ({ offer, card }) => {
21801
21873
  const bs = new ProductDetailsBottomSheet(this.widgetDeps, this.getBottomSheetParams());
21802
- const bsNode = bs.render();
21803
- if (!bsNode)
21804
- throw new BottomSheetMountingError();
21805
- this.slide.appendChild(bsNode);
21874
+ this.mountBottomSheet(bs);
21806
21875
  bs.open({ offer, isCartSupported: this.isCartSupported() });
21807
21876
  this.isBottomSheetOpened = true;
21808
21877
  this.disableHostUIInteraction();
@@ -21873,6 +21942,22 @@ class WidgetProductCarousel extends WidgetBase {
21873
21942
  });
21874
21943
  return button;
21875
21944
  }
21945
+ mountBottomSheet(bs) {
21946
+ if (this.productsView)
21947
+ return;
21948
+ const bsNode = bs.render();
21949
+ if (!bsNode)
21950
+ throw new BottomSheetMountingError();
21951
+ this.productsView = this.createProductsView();
21952
+ this.productsView.appendChild(bsNode);
21953
+ this.slide.appendChild(this.productsView);
21954
+ }
21955
+ createProductsView() {
21956
+ const containerView = document.createElement("div");
21957
+ containerView.classList.add("ias-products-container-view");
21958
+ containerView.dir = this.layoutDirection;
21959
+ return containerView;
21960
+ }
21876
21961
  async withTimeout(promise, timeout) {
21877
21962
  await Promise.race([promise, new Promise((resolve, reject) => setTimeout(reject, timeout))]);
21878
21963
  }
@@ -21888,10 +21973,7 @@ class WidgetProductCarousel extends WidgetBase {
21888
21973
  offerId: offerDto.offerId,
21889
21974
  quantity: 1,
21890
21975
  }), ADD_TO_CART_TIMEOUT);
21891
- const bsNode = bs.render();
21892
- if (!bsNode)
21893
- throw new BottomSheetMountingError();
21894
- this.slide.appendChild(bsNode);
21976
+ this.mountBottomSheet(bs);
21895
21977
  bs.open();
21896
21978
  this.isBottomSheetOpened = true;
21897
21979
  this.disableHostUIInteraction();
@@ -21929,6 +22011,10 @@ class WidgetProductCarousel extends WidgetBase {
21929
22011
  }
21930
22012
  this.isBottomSheetOpened = false;
21931
22013
  this.enableHostUIInteraction();
22014
+ if (this.productsView) {
22015
+ this.slide.removeChild(this.productsView);
22016
+ this.productsView = null;
22017
+ }
21932
22018
  },
21933
22019
  };
21934
22020
  }
@@ -25307,6 +25393,1314 @@ class WidgetReactions extends WidgetBase {
25307
25393
  static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLElement`, `Partial`, `WidgetCallbacks`, `WidgetDeps`]; }
25308
25394
  }
25309
25395
 
25396
+ var BrushType;
25397
+ (function (BrushType) {
25398
+ BrushType["Circle"] = "circle";
25399
+ BrushType["Line"] = "line";
25400
+ BrushType["Spray"] = "spray";
25401
+ })(BrushType || (BrushType = {}));
25402
+ var ScratchImageObjectFit;
25403
+ (function (ScratchImageObjectFit) {
25404
+ ScratchImageObjectFit["Cover"] = "cover";
25405
+ ScratchImageObjectFit["Contain"] = "contain";
25406
+ ScratchImageObjectFit["Fill"] = "fill";
25407
+ })(ScratchImageObjectFit || (ScratchImageObjectFit = {}));
25408
+
25409
+ /** Эталонная площадь холста (px²) для масштаба числа частиц spray. */
25410
+ const SPRAY_PARTICLE_CANVAS_REF_AREA = 800 * 600;
25411
+ /** Верхняя граница частиц за одно событие spray. */
25412
+ const SPRAY_PARTICLE_MAX = 12000;
25413
+ /** Шаг интерполяции вдоль отрезка LineBrush как доля радиуса (перекрытие дисков → ровные края). */
25414
+ const LINE_BRUSH_STEP_FACTOR = 0.42;
25415
+ /** Минимальный шаг в пикселях для очень маленьких кистей. */
25416
+ const LINE_BRUSH_STEP_MIN_PX = 1;
25417
+ class Brush {
25418
+ renderer;
25419
+ mask;
25420
+ constructor(renderer, mask) {
25421
+ this.renderer = renderer;
25422
+ this.mask = mask;
25423
+ }
25424
+ /** Круглая кисть в координатах bitmap (радиус = brushSize). */
25425
+ scratchDisc(x, y, radius) {
25426
+ const radiusSq = radius * radius;
25427
+ let newScratchedPixels = 0;
25428
+ for (let i = -radius; i <= radius; i++) {
25429
+ for (let j = -radius; j <= radius; j++) {
25430
+ if (i * i + j * j <= radiusSq) {
25431
+ newScratchedPixels += this.scratchPixelAt(x + i, y + j);
25432
+ }
25433
+ }
25434
+ }
25435
+ return newScratchedPixels;
25436
+ }
25437
+ scratchPixelAt(px, py) {
25438
+ return this.mask.scratchPixelXY(px, py, this.renderer.width, this.renderer.height);
25439
+ }
25440
+ get bitmapPixelCount() {
25441
+ return this.renderer.width * this.renderer.height;
25442
+ }
25443
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`ScratchRenderer`, `ScratchMask`]; }
25444
+ }
25445
+ class CircleBrush extends Brush {
25446
+ apply(x, y, radius, _prev) {
25447
+ return this.scratchDisc(x, y, radius);
25448
+ }
25449
+ }
25450
+ class SprayBrush extends Brush {
25451
+ apply(x, y, radius, _prev) {
25452
+ const canvasScale = Math.sqrt(Math.max(1, this.bitmapPixelCount / SPRAY_PARTICLE_CANVAS_REF_AREA));
25453
+ const particleCount = Math.min(SPRAY_PARTICLE_MAX, Math.max(16, Math.floor(radius * 8 * canvasScale)));
25454
+ let newScratchedPixels = 0;
25455
+ for (let i = 0; i < particleCount; i++) {
25456
+ const angle = Math.random() * Math.PI * 2;
25457
+ const r = Math.sqrt(Math.random()) * radius;
25458
+ const px = x + Math.cos(angle) * r;
25459
+ const py = y + Math.sin(angle) * r;
25460
+ newScratchedPixels += this.scratchPixelAt(px, py);
25461
+ }
25462
+ return newScratchedPixels;
25463
+ }
25464
+ }
25465
+ class LineBrush extends Brush {
25466
+ apply(x, y, radius, prev) {
25467
+ if (prev != null) {
25468
+ return this.scratchAlongLineSegment(prev.x, prev.y, x, y, radius);
25469
+ }
25470
+ return this.scratchDisc(x, y, radius);
25471
+ }
25472
+ /**
25473
+ * Линейная интерполяция вдоль отрезка: плотные круговые штампы перекрываются
25474
+ */
25475
+ scratchAlongLineSegment(x0, y0, x1, y1, radius) {
25476
+ const dx = x1 - x0;
25477
+ const dy = y1 - y0;
25478
+ const len = Math.hypot(dx, dy);
25479
+ if (len < 1e-6) {
25480
+ return this.scratchDisc(x1, y1, radius);
25481
+ }
25482
+ const step = Math.max(LINE_BRUSH_STEP_MIN_PX, radius * LINE_BRUSH_STEP_FACTOR);
25483
+ const ux = dx / len;
25484
+ const uy = dy / len;
25485
+ let newScratchedPixels = 0;
25486
+ for (let d = 0; d < len; d += step) {
25487
+ newScratchedPixels += this.scratchDisc(x0 + ux * d, y0 + uy * d, radius);
25488
+ }
25489
+ newScratchedPixels += this.scratchDisc(x1, y1, radius);
25490
+ return newScratchedPixels;
25491
+ }
25492
+ }
25493
+ function createBrush(type, renderer, mask) {
25494
+ switch (type) {
25495
+ case BrushType.Spray:
25496
+ return new SprayBrush(renderer, mask);
25497
+ case BrushType.Line:
25498
+ return new LineBrush(renderer, mask);
25499
+ case BrushType.Circle:
25500
+ default:
25501
+ return new CircleBrush(renderer, mask);
25502
+ }
25503
+ }
25504
+
25505
+ /** Парсинг первого значения border-radius в px (поддержка % от меньшей стороны прямоугольника). */
25506
+ function parseBorderRadiusPx(cssRadius, boxWidth, boxHeight) {
25507
+ const token = cssRadius.trim().split(/\s+/)[0];
25508
+ if (!token || token === "0" || token === "none") {
25509
+ return 0;
25510
+ }
25511
+ if (token.endsWith("%")) {
25512
+ const p = parseFloat(token) / 100;
25513
+ if (Number.isNaN(p))
25514
+ return 0;
25515
+ return p * Math.min(boxWidth, boxHeight);
25516
+ }
25517
+ if (token.endsWith("px")) {
25518
+ const v = parseFloat(token);
25519
+ return Number.isNaN(v) ? 0 : v;
25520
+ }
25521
+ return 0;
25522
+ }
25523
+ function isPointInRoundedRect(px, py, left, top, right, bottom, r) {
25524
+ const w = right - left;
25525
+ const h = bottom - top;
25526
+ if (w <= 0 || h <= 0) {
25527
+ return false;
25528
+ }
25529
+ r = Math.min(Math.max(0, r), w / 2, h / 2);
25530
+ if (px < left || px > right || py < top || py > bottom) {
25531
+ return false;
25532
+ }
25533
+ if (px >= left + r && px <= right - r) {
25534
+ return true;
25535
+ }
25536
+ if (py >= top + r && py <= bottom - r) {
25537
+ return true;
25538
+ }
25539
+ let cx;
25540
+ let cy;
25541
+ if (px < left + r && py < top + r) {
25542
+ cx = left + r;
25543
+ cy = top + r;
25544
+ }
25545
+ else if (px > right - r && py < top + r) {
25546
+ cx = right - r;
25547
+ cy = top + r;
25548
+ }
25549
+ else if (px < left + r && py > bottom - r) {
25550
+ cx = left + r;
25551
+ cy = bottom - r;
25552
+ }
25553
+ else {
25554
+ cx = right - r;
25555
+ cy = bottom - r;
25556
+ }
25557
+ const dx = px - cx;
25558
+ const dy = py - cy;
25559
+ return dx * dx + dy * dy <= r * r;
25560
+ }
25561
+ /**
25562
+ * Преобразование client ↔ layout ↔ bitmap и проверки попадания (в т.ч. с geometryAngleRad).
25563
+ */
25564
+ class CoordinateTransformer {
25565
+ canvas;
25566
+ getAngleRad;
25567
+ constructor(canvas, getAngleRad) {
25568
+ this.canvas = canvas;
25569
+ this.getAngleRad = getAngleRad;
25570
+ }
25571
+ /** Точка в клиенте → координаты в bitmap (целочисленные границы ячеек). */
25572
+ toCanvas(clientX, clientY) {
25573
+ const layout = this.getCanvasLayoutSizeCss();
25574
+ if (!layout) {
25575
+ return { x: 0, y: 0 };
25576
+ }
25577
+ const rect = this.canvas.getBoundingClientRect();
25578
+ const cx = rect.left + rect.width / 2;
25579
+ const cy = rect.top + rect.height / 2;
25580
+ const vx = clientX - cx;
25581
+ const vy = clientY - cy;
25582
+ const angle = this.getAngleRad();
25583
+ const c = Math.cos(angle);
25584
+ const s = Math.sin(angle);
25585
+ const { lx, ly } = this.clientDeltaToLayoutLocal(vx, vy, c, s);
25586
+ const xCss = lx + layout.w / 2;
25587
+ const yCss = ly + layout.h / 2;
25588
+ const { x, y } = this.layoutCssToCanvas(xCss, yCss, layout);
25589
+ return this.clampToCanvasBitmap(x, y);
25590
+ }
25591
+ /** Точка в клиенте попадает в прямоугольник canvas в layout-пространстве (с учётом поворота). */
25592
+ isInside(clientX, clientY) {
25593
+ const layout = this.getCanvasLayoutSizeCss();
25594
+ if (!layout) {
25595
+ return false;
25596
+ }
25597
+ const rect = this.canvas.getBoundingClientRect();
25598
+ const cx = rect.left + rect.width / 2;
25599
+ const cy = rect.top + rect.height / 2;
25600
+ const vx = clientX - cx;
25601
+ const vy = clientY - cy;
25602
+ const angle = this.getAngleRad();
25603
+ const c = Math.cos(angle);
25604
+ const s = Math.sin(angle);
25605
+ const { lx, ly } = this.clientDeltaToLayoutLocal(vx, vy, c, s);
25606
+ const xCss = lx + layout.w / 2;
25607
+ const yCss = ly + layout.h / 2;
25608
+ return xCss >= 0 && xCss <= layout.w && yCss >= 0 && yCss <= layout.h;
25609
+ }
25610
+ /**
25611
+ * Центры ячеек bitmap: учитываются в прогрессе, если центр в viewport (slide) и в скруглённой карте (clipElement).
25612
+ * Без скругления и без viewport — null (прогресс по всем scratchable).
25613
+ */
25614
+ buildProgressRegionFlags(viewportElement, clipElement, width, height) {
25615
+ if (width <= 0 || height <= 0) {
25616
+ return null;
25617
+ }
25618
+ const layout = this.getCanvasLayoutSizeCss();
25619
+ if (!layout) {
25620
+ return null;
25621
+ }
25622
+ const clipRect = clipElement.getBoundingClientRect();
25623
+ const clipStyle = getComputedStyle(clipElement);
25624
+ const radiusPx = parseBorderRadiusPx(clipStyle.borderRadius || "0", clipRect.width, clipRect.height);
25625
+ let vr = null;
25626
+ if (viewportElement) {
25627
+ const viewportRect = viewportElement.getBoundingClientRect();
25628
+ if (viewportRect.width > 0 && viewportRect.height > 0) {
25629
+ vr = viewportRect;
25630
+ }
25631
+ }
25632
+ if (!vr && radiusPx <= 0) {
25633
+ return null;
25634
+ }
25635
+ const n = width * height;
25636
+ const flags = new Uint8Array(n);
25637
+ const rect = this.canvas.getBoundingClientRect();
25638
+ const cx = rect.left + rect.width / 2;
25639
+ const cy = rect.top + rect.height / 2;
25640
+ const angle = this.getAngleRad();
25641
+ const c = Math.cos(angle);
25642
+ const s = Math.sin(angle);
25643
+ let minIx = 0;
25644
+ let maxIx = width - 1;
25645
+ let minIy = 0;
25646
+ let maxIy = height - 1;
25647
+ if (vr) {
25648
+ const corners = [
25649
+ [vr.left, vr.top],
25650
+ [vr.right, vr.top],
25651
+ [vr.left, vr.bottom],
25652
+ [vr.right, vr.bottom],
25653
+ ];
25654
+ let mix = Infinity;
25655
+ let maxx = -Infinity;
25656
+ let miy = Infinity;
25657
+ let may = -Infinity;
25658
+ for (let i = 0; i < corners.length; i++) {
25659
+ const [ccx, ccy] = corners[i];
25660
+ const { ix, iy } = this.clientToBitmapIndicesFloat(ccx, ccy, layout, cx, cy, c, s);
25661
+ if (ix < mix)
25662
+ mix = ix;
25663
+ if (ix > maxx)
25664
+ maxx = ix;
25665
+ if (iy < miy)
25666
+ miy = iy;
25667
+ if (iy > may)
25668
+ may = iy;
25669
+ }
25670
+ minIx = Math.max(0, Math.floor(mix));
25671
+ maxIx = Math.min(width - 1, Math.ceil(maxx));
25672
+ minIy = Math.max(0, Math.floor(miy));
25673
+ maxIy = Math.min(height - 1, Math.ceil(may));
25674
+ }
25675
+ const cl = clipRect.left;
25676
+ const ct = clipRect.top;
25677
+ const cr = clipRect.right;
25678
+ const cb = clipRect.bottom;
25679
+ for (let iy = minIy; iy <= maxIy; iy++) {
25680
+ const row = iy * width;
25681
+ for (let ix = minIx; ix <= maxIx; ix++) {
25682
+ const client = this.bitmapCellCenterClientFromLayout(ix, iy, layout, cx, cy, c, s);
25683
+ const idx = row + ix;
25684
+ const inViewport = vr === null || (client.x >= vr.left && client.x < vr.right && client.y >= vr.top && client.y < vr.bottom);
25685
+ const inRounded = radiusPx <= 0 || isPointInRoundedRect(client.x, client.y, cl, ct, cr, cb, radiusPx);
25686
+ flags[idx] = inViewport && inRounded ? 1 : 0;
25687
+ }
25688
+ }
25689
+ return flags;
25690
+ }
25691
+ getCanvasLayoutSizeCss() {
25692
+ const w = this.canvas.offsetWidth;
25693
+ const h = this.canvas.offsetHeight;
25694
+ if (w <= 0 || h <= 0) {
25695
+ return null;
25696
+ }
25697
+ return { w, h };
25698
+ }
25699
+ canvasToLayoutCss(x, y, layout) {
25700
+ const w = this.canvas.width;
25701
+ const h = this.canvas.height;
25702
+ return {
25703
+ xCss: (x * layout.w) / w,
25704
+ yCss: (y * layout.h) / h,
25705
+ };
25706
+ }
25707
+ layoutCssToCanvas(xCss, yCss, layout) {
25708
+ const w = this.canvas.width;
25709
+ const h = this.canvas.height;
25710
+ return {
25711
+ x: (xCss * w) / layout.w,
25712
+ y: (yCss * h) / layout.h,
25713
+ };
25714
+ }
25715
+ clientDeltaToLayoutLocal(vx, vy, c, s) {
25716
+ return {
25717
+ lx: vx * c + vy * s,
25718
+ ly: -vx * s + vy * c,
25719
+ };
25720
+ }
25721
+ layoutLocalDeltaToClientDelta(lx, ly, c, s) {
25722
+ return {
25723
+ vx: lx * c - ly * s,
25724
+ vy: lx * s + ly * c,
25725
+ };
25726
+ }
25727
+ clampToCanvasBitmap(x, y) {
25728
+ return {
25729
+ x: Math.max(0, Math.min(this.canvas.width - 1, x)),
25730
+ y: Math.max(0, Math.min(this.canvas.height - 1, y)),
25731
+ };
25732
+ }
25733
+ bitmapCellCenterClientFromLayout(ix, iy, layout, cx, cy, c, s) {
25734
+ const { xCss, yCss } = this.canvasToLayoutCss(ix + 0.5, iy + 0.5, layout);
25735
+ const lx = xCss - layout.w / 2;
25736
+ const ly = yCss - layout.h / 2;
25737
+ const { vx, vy } = this.layoutLocalDeltaToClientDelta(lx, ly, c, s);
25738
+ return { x: cx + vx, y: cy + vy };
25739
+ }
25740
+ clientToBitmapIndicesFloat(clientX, clientY, layout, cx, cy, c, s) {
25741
+ const vx = clientX - cx;
25742
+ const vy = clientY - cy;
25743
+ const { lx, ly } = this.clientDeltaToLayoutLocal(vx, vy, c, s);
25744
+ const xCss = lx + layout.w / 2;
25745
+ const yCss = ly + layout.h / 2;
25746
+ const { x, y } = this.layoutCssToCanvas(xCss, yCss, layout);
25747
+ return { ix: x - 0.5, iy: y - 0.5 };
25748
+ }
25749
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLCanvasElement`, `() => number`]; }
25750
+ }
25751
+
25752
+ class Effect {
25753
+ cancel() { }
25754
+ }
25755
+ /**
25756
+ * Плавное исчезновение нестёртых пикселей после достижения порога стирания.
25757
+ */
25758
+ class FadeOutEffect extends Effect {
25759
+ context;
25760
+ options;
25761
+ timeoutId = null;
25762
+ rafId = null;
25763
+ constructor(context, options) {
25764
+ super();
25765
+ this.context = context;
25766
+ this.options = options;
25767
+ }
25768
+ start() {
25769
+ this.cancel();
25770
+ this.timeoutId = setTimeout(() => {
25771
+ this.timeoutId = null;
25772
+ this.runFadeAnimation();
25773
+ }, this.options.fadeTimeout());
25774
+ }
25775
+ cancel() {
25776
+ if (this.timeoutId != null) {
25777
+ clearTimeout(this.timeoutId);
25778
+ this.timeoutId = null;
25779
+ }
25780
+ if (this.rafId != null) {
25781
+ cancelAnimationFrame(this.rafId);
25782
+ this.rafId = null;
25783
+ }
25784
+ }
25785
+ runFadeAnimation() {
25786
+ const context = this.context;
25787
+ const ctx2d = context.ctx();
25788
+ const startTime = performance.now();
25789
+ const startMask = new Uint8Array(context.mask);
25790
+ const total = context.totalPixels;
25791
+ const fadeDuration = this.options.fadeDuration();
25792
+ const animate = (currentTime) => {
25793
+ const elapsed = currentTime - startTime;
25794
+ const progress = Math.min(1, elapsed / fadeDuration);
25795
+ this.applyFadeFrame(ctx2d, context.canvasWidth, context.canvasHeight, total, progress, startMask, context.scratchablePixelFlags);
25796
+ if (progress < 1) {
25797
+ this.rafId = requestAnimationFrame(animate);
25798
+ }
25799
+ else {
25800
+ this.rafId = null;
25801
+ this.completeFade(context);
25802
+ this.options.onComplete();
25803
+ }
25804
+ };
25805
+ this.rafId = requestAnimationFrame(animate);
25806
+ }
25807
+ applyFadeFrame(ctx2d, width, height, totalPixels, progress, startMask, scratchablePixelFlags) {
25808
+ const imageData = ctx2d.getImageData(0, 0, width, height);
25809
+ const data = imageData.data;
25810
+ const easedProgress = 1 - Math.pow(1 - progress, 2);
25811
+ for (let i = 0; i < totalPixels; i++) {
25812
+ if (scratchablePixelFlags !== undefined && scratchablePixelFlags[i] === 0) {
25813
+ continue;
25814
+ }
25815
+ if (startMask[i] === 0) {
25816
+ const alpha = Math.floor(255 * (1 - easedProgress));
25817
+ data[i * 4 + 3] = alpha;
25818
+ }
25819
+ else if (startMask[i] === 1) {
25820
+ data[i * 4 + 3] = 0;
25821
+ }
25822
+ }
25823
+ ctx2d.putImageData(imageData, 0, 0);
25824
+ }
25825
+ completeFade(context) {
25826
+ const ctx2d = context.ctx();
25827
+ const imageData = ctx2d.getImageData(0, 0, context.canvasWidth, context.canvasHeight);
25828
+ const data = imageData.data;
25829
+ const total = context.totalPixels;
25830
+ for (let i = 0; i < total; i++) {
25831
+ data[i * 4 + 3] = 0;
25832
+ }
25833
+ ctx2d.putImageData(imageData, 0, 0);
25834
+ for (let i = 0; i < total; i++) {
25835
+ context.mask[i] = 1;
25836
+ }
25837
+ }
25838
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`ScratchEffectContext`, `FadeOutEffectOptions`]; }
25839
+ }
25840
+
25841
+ /** Обёртка над завершающим fade-эффектом (таймаут + анимация). */
25842
+ class EffectController {
25843
+ effect;
25844
+ constructor(context, options) {
25845
+ this.effect = new FadeOutEffect(context, options);
25846
+ }
25847
+ startComplete() {
25848
+ this.effect.start();
25849
+ }
25850
+ cancel() {
25851
+ this.effect.cancel();
25852
+ }
25853
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`ScratchEffectContext`, `FadeOutEffectOptions`]; }
25854
+ }
25855
+
25856
+ class Pixel {
25857
+ constructor() { }
25858
+ static index(x, y, bitmapWidth) {
25859
+ return y * bitmapWidth + x;
25860
+ }
25861
+ static x(index, bitmapWidth) {
25862
+ return index % bitmapWidth;
25863
+ }
25864
+ static y(index, bitmapWidth) {
25865
+ return Math.floor(index / bitmapWidth);
25866
+ }
25867
+ static get [Symbol.for("___CTOR_ARGS___")]() { return []; }
25868
+ }
25869
+
25870
+ /**
25871
+ * Маска стирания, флаги «стираемых» пикселей (alpha слоя) и viewport для подсчёта прогресса.
25872
+ */
25873
+ class ScratchMask {
25874
+ _mask;
25875
+ _scratchablePixelFlags;
25876
+ _inViewportFlags;
25877
+ _totalPixels;
25878
+ /** Пиксели слоя с alpha > 0 после отрисовки. */
25879
+ scratchablePixelTotal = 0;
25880
+ /** Стираемые пиксели внутри viewport. */
25881
+ viewportPixelTotal = 0;
25882
+ constructor(width, height, viewportFlags) {
25883
+ this._totalPixels = width * height;
25884
+ this._mask = new Uint8Array(this._totalPixels);
25885
+ this._inViewportFlags = viewportFlags;
25886
+ }
25887
+ get mask() {
25888
+ return this._mask;
25889
+ }
25890
+ get scratchablePixelFlags() {
25891
+ return this._scratchablePixelFlags;
25892
+ }
25893
+ get totalPixels() {
25894
+ return this._totalPixels;
25895
+ }
25896
+ get inViewportFlags() {
25897
+ return this._inViewportFlags;
25898
+ }
25899
+ /**
25900
+ * После отрисовки базового слоя: помечаем ячейки с ненулевым alpha (учёт PNG).
25901
+ */
25902
+ loadScratchableFromImageData(imageData, width, height) {
25903
+ if (width <= 0 || height <= 0) {
25904
+ this._scratchablePixelFlags = new Uint8Array(0);
25905
+ this.scratchablePixelTotal = 0;
25906
+ return;
25907
+ }
25908
+ const n = width * height;
25909
+ if (!this._scratchablePixelFlags || this._scratchablePixelFlags.length !== n) {
25910
+ this._scratchablePixelFlags = new Uint8Array(n);
25911
+ }
25912
+ const data = imageData.data;
25913
+ let total = 0;
25914
+ for (let i = 0; i < n; i++) {
25915
+ const scratchable = data[i * 4 + 3] > 0 ? 1 : 0;
25916
+ this._scratchablePixelFlags[i] = scratchable;
25917
+ total += scratchable;
25918
+ }
25919
+ this.scratchablePixelTotal = total;
25920
+ }
25921
+ /** Пересчитать знаменатель прогресса по scratchable ∩ viewport. */
25922
+ updateViewportDenominator() {
25923
+ if (!this._inViewportFlags) {
25924
+ this.viewportPixelTotal = 0;
25925
+ return;
25926
+ }
25927
+ let den = 0;
25928
+ const n = this._totalPixels;
25929
+ const scratchable = this._scratchablePixelFlags;
25930
+ const inVp = this._inViewportFlags;
25931
+ for (let i = 0; i < n; i++) {
25932
+ if (scratchable[i] && inVp[i]) {
25933
+ den++;
25934
+ }
25935
+ }
25936
+ this.viewportPixelTotal = den;
25937
+ }
25938
+ /**
25939
+ * Стирание по индексу буфера; возвращает вклад в прогресс (0 или 1).
25940
+ */
25941
+ scratchPixel(index) {
25942
+ return this.scratchPixelIndexIfUnset(index);
25943
+ }
25944
+ scratchPixelXY(x, y, bitmapWidth, bitmapHeight) {
25945
+ const pixelX = Math.floor(x);
25946
+ const pixelY = Math.floor(y);
25947
+ if (pixelX < 0 || pixelX >= bitmapWidth || pixelY < 0 || pixelY >= bitmapHeight) {
25948
+ return 0;
25949
+ }
25950
+ const idx = Pixel.index(pixelX, pixelY, bitmapWidth);
25951
+ return this.scratchPixelIndexIfUnset(idx);
25952
+ }
25953
+ scratchPixelIndexIfUnset(pixelIndex) {
25954
+ if (pixelIndex < 0 || pixelIndex >= this._totalPixels) {
25955
+ return 0;
25956
+ }
25957
+ if (this._mask[pixelIndex] === 0) {
25958
+ this._mask[pixelIndex] = 1;
25959
+ if (this._scratchablePixelFlags[pixelIndex] === 0) {
25960
+ return 0;
25961
+ }
25962
+ if (!this._inViewportFlags) {
25963
+ return 1;
25964
+ }
25965
+ return this._inViewportFlags[pixelIndex] !== 0 ? 1 : 0;
25966
+ }
25967
+ return 0;
25968
+ }
25969
+ /** Число стёртых «учитываемых» пикселей в viewport (для синхронизации после reset маски). */
25970
+ countScratchedForProgress() {
25971
+ const inVp = this._inViewportFlags;
25972
+ if (!inVp) {
25973
+ return 0;
25974
+ }
25975
+ let num = 0;
25976
+ const n = this._totalPixels;
25977
+ const mask = this._mask;
25978
+ const scratchable = this._scratchablePixelFlags;
25979
+ for (let i = 0; i < n; i++) {
25980
+ if (mask[i] && scratchable[i] && inVp[i]) {
25981
+ num++;
25982
+ }
25983
+ }
25984
+ return num;
25985
+ }
25986
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`number`, `number`, `Uint8Array | null`]; }
25987
+ }
25988
+
25989
+ const NOISE_DENSITY_AT_FULL = 800;
25990
+ /**
25991
+ * Отрисовка базового слоя и применение маски к ImageData
25992
+ */
25993
+ class ScratchRenderer {
25994
+ canvas;
25995
+ _ctx;
25996
+ constructor(canvas) {
25997
+ this.canvas = canvas;
25998
+ const ctx = canvas.getContext("2d");
25999
+ if (!ctx) {
26000
+ throw new Error("Unable to get 2D context");
26001
+ }
26002
+ this._ctx = ctx;
26003
+ }
26004
+ get width() {
26005
+ return this.canvas.width;
26006
+ }
26007
+ get height() {
26008
+ return this.canvas.height;
26009
+ }
26010
+ getContext() {
26011
+ return this._ctx;
26012
+ }
26013
+ setupSizeFromContainer(container) {
26014
+ const w = container.offsetWidth;
26015
+ const h = container.offsetHeight;
26016
+ this.canvas.width = w;
26017
+ this.canvas.height = h;
26018
+ this.canvas.style.width = `${w}px`;
26019
+ this.canvas.style.height = `${h}px`;
26020
+ }
26021
+ drawBaseLayer(background, options = {}) {
26022
+ this.clearCanvas();
26023
+ const imageFit = options.imageObjectFit ?? ScratchImageObjectFit.Cover;
26024
+ this.drawBackground(background, imageFit);
26025
+ this.addTextureNoise(options.texture?.noiseDensity ?? 0);
26026
+ }
26027
+ applyMask(mask, totalPixels) {
26028
+ if (this.width <= 0 || this.height <= 0)
26029
+ return;
26030
+ const imageData = this.getImageData();
26031
+ const data = imageData.data;
26032
+ for (let i = 0; i < totalPixels; i++) {
26033
+ if (mask[i] === 0)
26034
+ continue;
26035
+ data[i * 4 + 3] = 0;
26036
+ }
26037
+ this._ctx.putImageData(imageData, 0, 0);
26038
+ }
26039
+ getImageData() {
26040
+ const w = this.canvas.width;
26041
+ const h = this.canvas.height;
26042
+ /** Нулевые размеры canvas — не вызываем ctx.getImageData. */
26043
+ if (w <= 0 || h <= 0) {
26044
+ return new ImageData(1, 1);
26045
+ }
26046
+ return this._ctx.getImageData(0, 0, w, h);
26047
+ }
26048
+ clearCanvas() {
26049
+ this._ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
26050
+ }
26051
+ drawBackground(background, imageObjectFit) {
26052
+ if (!background)
26053
+ return;
26054
+ switch (background.type) {
26055
+ case "color":
26056
+ this.drawSolidBackground(background.value);
26057
+ break;
26058
+ case "gradient":
26059
+ this.drawGradientBackground(background.start, background.end);
26060
+ break;
26061
+ case "image":
26062
+ this.drawImageBackground(background.value, imageObjectFit);
26063
+ break;
26064
+ }
26065
+ }
26066
+ drawSolidBackground(color) {
26067
+ this._ctx.fillStyle = color;
26068
+ this._ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
26069
+ }
26070
+ drawGradientBackground(startColor, endColor) {
26071
+ const gradient = this._ctx.createLinearGradient(0, 0, this.canvas.width, this.canvas.height);
26072
+ gradient.addColorStop(0, startColor);
26073
+ gradient.addColorStop(1, endColor);
26074
+ this._ctx.fillStyle = gradient;
26075
+ this._ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
26076
+ }
26077
+ drawImageBackground(image, fit) {
26078
+ const cw = this.canvas.width;
26079
+ const ch = this.canvas.height;
26080
+ const iw = image.naturalWidth || image.width;
26081
+ const ih = image.naturalHeight || image.height;
26082
+ if (iw <= 0 || ih <= 0 || cw <= 0 || ch <= 0) {
26083
+ return;
26084
+ }
26085
+ if (fit === ScratchImageObjectFit.Fill) {
26086
+ this._ctx.drawImage(image, 0, 0, cw, ch);
26087
+ return;
26088
+ }
26089
+ let scale;
26090
+ if (fit === ScratchImageObjectFit.Contain) {
26091
+ scale = Math.min(cw / iw, ch / ih);
26092
+ }
26093
+ else {
26094
+ scale = Math.max(cw / iw, ch / ih);
26095
+ }
26096
+ const dw = iw * scale;
26097
+ const dh = ih * scale;
26098
+ const dx = (cw - dw) / 2;
26099
+ const dy = (ch - dh) / 2;
26100
+ this._ctx.drawImage(image, 0, 0, iw, ih, dx, dy, dw, dh);
26101
+ }
26102
+ addTextureNoise(noiseDensityPercent) {
26103
+ const percent = Math.min(100, Math.max(0, noiseDensityPercent));
26104
+ const iterations = Math.round((percent / 100) * NOISE_DENSITY_AT_FULL);
26105
+ for (let i = 0; i < iterations; i++) {
26106
+ this.drawNoisePixel();
26107
+ }
26108
+ }
26109
+ drawNoisePixel() {
26110
+ this._ctx.fillStyle = `rgba(0,0,0,${Math.random() * 0.3})`;
26111
+ const x = Math.random() * this.canvas.width;
26112
+ const y = Math.random() * this.canvas.height;
26113
+ this._ctx.fillRect(x, y, 2, 2);
26114
+ }
26115
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLCanvasElement`]; }
26116
+ }
26117
+
26118
+ class ScratchLayer {
26119
+ canvas;
26120
+ renderer;
26121
+ maskModel;
26122
+ transformer;
26123
+ mouseContainer;
26124
+ inputAttached = false;
26125
+ scratchedPixels = 0;
26126
+ brush;
26127
+ completeEffect;
26128
+ options = {
26129
+ brushSize: 35,
26130
+ brushType: BrushType.Circle,
26131
+ scratchThreshold: 0.7,
26132
+ onComplete: () => { },
26133
+ onProgress: () => { },
26134
+ onMouseDown: () => { },
26135
+ onMouseUp: () => { },
26136
+ backgroundColor: null,
26137
+ backgroundGradient: null,
26138
+ texture: {
26139
+ noiseDensity: 0,
26140
+ },
26141
+ backgroundImage: null,
26142
+ imageObjectFit: ScratchImageObjectFit.Cover,
26143
+ fadeTimeout: 0,
26144
+ onFadeComplete: null,
26145
+ fadeDuration: 300,
26146
+ mouseContainer: null,
26147
+ geometryAngleRad: null,
26148
+ viewportElement: null,
26149
+ progressClipElement: null,
26150
+ };
26151
+ background = null;
26152
+ isDrawing = false;
26153
+ isRevealed = false;
26154
+ isAnimating = false;
26155
+ lastScratchX = null;
26156
+ lastScratchY = null;
26157
+ container;
26158
+ _enabled = true;
26159
+ geometryAngleRad = 0;
26160
+ viewportElement = null;
26161
+ get canvasWidth() {
26162
+ return this.renderer.width;
26163
+ }
26164
+ get canvasHeight() {
26165
+ return this.renderer.height;
26166
+ }
26167
+ get totalPixels() {
26168
+ return this.maskModel.totalPixels;
26169
+ }
26170
+ get mask() {
26171
+ return this.maskModel.mask;
26172
+ }
26173
+ get scratchablePixelFlags() {
26174
+ return this.maskModel.scratchablePixelFlags;
26175
+ }
26176
+ ctx() {
26177
+ return this.renderer.getContext();
26178
+ }
26179
+ constructor(container, options = {}) {
26180
+ this.container = container;
26181
+ this.canvas = this.mountCanvas(this.container);
26182
+ this.renderer = new ScratchRenderer(this.canvas);
26183
+ this.transformer = new CoordinateTransformer(this.canvas, () => this.geometryAngleRad);
26184
+ this.updateOptions(options);
26185
+ const mouseContainer = this.options.mouseContainer;
26186
+ if (!mouseContainer) {
26187
+ throw new Error("ScratchLayer: mouseContainer is required");
26188
+ }
26189
+ this.mouseContainer = mouseContainer;
26190
+ const layer = this;
26191
+ const fadeOptions = {
26192
+ fadeTimeout: () => layer.options.fadeTimeout,
26193
+ fadeDuration: () => layer.options.fadeDuration,
26194
+ onComplete: () => {
26195
+ layer.isAnimating = false;
26196
+ layer.options.onFadeComplete?.();
26197
+ },
26198
+ };
26199
+ this.completeEffect = new EffectController(this, fadeOptions);
26200
+ void this.init();
26201
+ }
26202
+ reset() {
26203
+ this.completeEffect.cancel();
26204
+ this.removeTransientMouseListeners();
26205
+ this.removeTransientTouchListeners();
26206
+ this.isDrawing = false;
26207
+ this.isRevealed = false;
26208
+ this.isAnimating = false;
26209
+ this.lastScratchX = null;
26210
+ this.lastScratchY = null;
26211
+ this.scratchedPixels = 0;
26212
+ this.renderer.setupSizeFromContainer(this.container);
26213
+ this.renderer.drawBaseLayer(this.background, {
26214
+ texture: this.options.texture,
26215
+ imageObjectFit: this.options.imageObjectFit,
26216
+ });
26217
+ this.rebuildMaskAfterBaseDraw();
26218
+ }
26219
+ enable() {
26220
+ this._enabled = true;
26221
+ }
26222
+ disable() {
26223
+ this._enabled = false;
26224
+ }
26225
+ updateOptions(options) {
26226
+ this.options = {
26227
+ brushSize: options.brushSize ?? this.options.brushSize,
26228
+ brushType: options.brushType ?? this.options.brushType,
26229
+ scratchThreshold: options.scratchThreshold ?? this.options.scratchThreshold,
26230
+ onComplete: options.onComplete ?? this.options.onComplete,
26231
+ onProgress: options.onProgress ?? this.options.onProgress,
26232
+ onMouseDown: options.onMouseDown ?? this.options.onMouseDown,
26233
+ onMouseUp: options.onMouseUp ?? this.options.onMouseUp,
26234
+ backgroundColor: options.backgroundColor ?? this.options.backgroundColor,
26235
+ backgroundGradient: options.backgroundGradient ?? this.options.backgroundGradient,
26236
+ texture: options.texture ?? this.options.texture,
26237
+ backgroundImage: options.backgroundImage ?? this.options.backgroundImage,
26238
+ imageObjectFit: options.imageObjectFit ?? this.options.imageObjectFit,
26239
+ fadeTimeout: options.fadeTimeout ?? this.options.fadeTimeout,
26240
+ onFadeComplete: options.onFadeComplete ?? this.options.onFadeComplete,
26241
+ fadeDuration: options.fadeDuration ?? this.options.fadeDuration,
26242
+ mouseContainer: options.mouseContainer ?? this.options.mouseContainer,
26243
+ geometryAngleRad: options.geometryAngleRad ?? this.options.geometryAngleRad,
26244
+ viewportElement: options.viewportElement !== undefined ? options.viewportElement : this.options.viewportElement,
26245
+ progressClipElement: options.progressClipElement !== undefined ? options.progressClipElement : this.options.progressClipElement,
26246
+ };
26247
+ const rawAngle = this.options.geometryAngleRad;
26248
+ this.geometryAngleRad = rawAngle != null && !Number.isNaN(rawAngle) ? rawAngle : 0;
26249
+ this.viewportElement = this.options.viewportElement;
26250
+ if (options.mouseContainer != null && options.mouseContainer !== this.mouseContainer) {
26251
+ const wasAttached = this.inputAttached;
26252
+ if (wasAttached) {
26253
+ this.detachInputListeners();
26254
+ }
26255
+ this.mouseContainer = options.mouseContainer;
26256
+ if (wasAttached) {
26257
+ this.attachInputListeners();
26258
+ }
26259
+ }
26260
+ }
26261
+ updateSize() {
26262
+ this.destroy();
26263
+ void this.init();
26264
+ }
26265
+ destroy() {
26266
+ this.completeEffect.cancel();
26267
+ if (this.isAnimating) {
26268
+ this.isAnimating = false;
26269
+ }
26270
+ this.detachInputListeners();
26271
+ }
26272
+ mountCanvas(container) {
26273
+ const canvas = document.createElement("canvas");
26274
+ canvas.style.cursor = "pointer";
26275
+ canvas.style.display = "block";
26276
+ container.appendChild(canvas);
26277
+ return canvas;
26278
+ }
26279
+ attachInputListeners() {
26280
+ if (this.inputAttached)
26281
+ return;
26282
+ this.inputAttached = true;
26283
+ this.canvas.addEventListener("mousedown", this.handleLayerMouseDown);
26284
+ this.canvas.addEventListener("touchstart", this.handleLayerTouchStart);
26285
+ }
26286
+ detachInputListeners() {
26287
+ if (!this.inputAttached)
26288
+ return;
26289
+ this.inputAttached = false;
26290
+ this.removeTransientMouseListeners();
26291
+ this.removeTransientTouchListeners();
26292
+ this.canvas.removeEventListener("mousedown", this.handleLayerMouseDown);
26293
+ this.canvas.removeEventListener("touchstart", this.handleLayerTouchStart);
26294
+ }
26295
+ removeTransientMouseListeners() {
26296
+ this.mouseContainer.removeEventListener("mousemove", this.handleLayerMouseMove);
26297
+ this.mouseContainer.removeEventListener("mouseup", this.handleLayerMouseUpOnce);
26298
+ }
26299
+ removeTransientTouchListeners() {
26300
+ this.canvas.removeEventListener("touchmove", this.handleLayerTouchMove);
26301
+ this.canvas.removeEventListener("touchend", this.handleLayerTouchEndOnce);
26302
+ this.mouseContainer.removeEventListener("touchcancel", this.handleLayerTouchEndOnce);
26303
+ }
26304
+ handleLayerMouseDown = (e) => {
26305
+ this.onPointerStart(e);
26306
+ if (this.isDrawing) {
26307
+ this.removeTransientMouseListeners();
26308
+ this.mouseContainer.addEventListener("mousemove", this.handleLayerMouseMove);
26309
+ this.mouseContainer.addEventListener("mouseup", this.handleLayerMouseUpOnce);
26310
+ }
26311
+ };
26312
+ handleLayerMouseMove = (e) => {
26313
+ this.onPointerMove(e);
26314
+ };
26315
+ handleLayerMouseUpOnce = (_e) => {
26316
+ this.removeTransientMouseListeners();
26317
+ this.onPointerEnd();
26318
+ };
26319
+ handleLayerTouchStart = (e) => {
26320
+ this.onPointerStart(e);
26321
+ if (this.isDrawing) {
26322
+ this.removeTransientTouchListeners();
26323
+ this.canvas.addEventListener("touchmove", this.handleLayerTouchMove);
26324
+ this.canvas.addEventListener("touchend", this.handleLayerTouchEndOnce);
26325
+ this.mouseContainer.addEventListener("touchcancel", this.handleLayerTouchEndOnce);
26326
+ }
26327
+ };
26328
+ handleLayerTouchMove = (e) => {
26329
+ this.onPointerMove(e);
26330
+ };
26331
+ handleLayerTouchEndOnce = (_e) => {
26332
+ this.removeTransientTouchListeners();
26333
+ this.onPointerEnd();
26334
+ };
26335
+ async init() {
26336
+ await this.resolveBackgroundImage();
26337
+ this.renderer.setupSizeFromContainer(this.container);
26338
+ this.renderer.drawBaseLayer(this.background, {
26339
+ texture: this.options.texture,
26340
+ imageObjectFit: this.options.imageObjectFit,
26341
+ });
26342
+ this.rebuildMaskAfterBaseDraw();
26343
+ this.attachInputListeners();
26344
+ }
26345
+ /** После `drawBaseLayer`: viewport-флаги → новая маска → scratchable → прогресс → кисть → отрисовка маски на canvas. */
26346
+ rebuildMaskAfterBaseDraw() {
26347
+ const w = this.renderer.width;
26348
+ const h = this.renderer.height;
26349
+ const clipEl = this.options.progressClipElement ?? this.container;
26350
+ const vpFlags = this.transformer.buildProgressRegionFlags(this.viewportElement, clipEl, w, h);
26351
+ this.maskModel = new ScratchMask(w, h, vpFlags);
26352
+ this.maskModel.loadScratchableFromImageData(this.renderer.getImageData(), w, h);
26353
+ this.maskModel.updateViewportDenominator();
26354
+ this.syncProgressFromMask();
26355
+ this.brush = createBrush(this.options.brushType, this.renderer, this.maskModel);
26356
+ this.renderer.applyMask(this.maskModel.mask, this.maskModel.totalPixels);
26357
+ }
26358
+ syncProgressFromMask() {
26359
+ if (this.maskModel.inViewportFlags) {
26360
+ this.scratchedPixels = this.maskModel.countScratchedForProgress();
26361
+ }
26362
+ else {
26363
+ this.scratchedPixels = 0;
26364
+ }
26365
+ }
26366
+ async resolveBackgroundImage() {
26367
+ if (this.options.backgroundImage) {
26368
+ this.background = { type: "image", value: this.options.backgroundImage };
26369
+ return;
26370
+ }
26371
+ if (this.options.backgroundGradient) {
26372
+ this.background = {
26373
+ type: "gradient",
26374
+ start: this.options.backgroundGradient.start,
26375
+ end: this.options.backgroundGradient.end,
26376
+ };
26377
+ return;
26378
+ }
26379
+ if (this.options.backgroundColor) {
26380
+ this.background = { type: "color", value: this.options.backgroundColor };
26381
+ return;
26382
+ }
26383
+ this.background = {
26384
+ type: "gradient",
26385
+ start: "#c0c0c0",
26386
+ end: "#808080",
26387
+ };
26388
+ }
26389
+ onPointerStart(e) {
26390
+ if (!this._enabled)
26391
+ return;
26392
+ e.preventDefault();
26393
+ if (this.isTouchEvent(e)) {
26394
+ this.startDrawingTouch(e);
26395
+ }
26396
+ else {
26397
+ this.startDrawingMouse(e);
26398
+ }
26399
+ this.options.onMouseDown?.();
26400
+ }
26401
+ onPointerMove(e) {
26402
+ if (!this._enabled)
26403
+ return;
26404
+ if (e instanceof MouseEvent) {
26405
+ if (!this.transformer.isInside(e.clientX, e.clientY))
26406
+ return;
26407
+ this.handleDrawingMouse(e);
26408
+ }
26409
+ else {
26410
+ this.handleDrawingTouch(e);
26411
+ }
26412
+ }
26413
+ onPointerEnd() {
26414
+ this.isDrawing = false;
26415
+ this.lastScratchX = null;
26416
+ this.lastScratchY = null;
26417
+ this.options.onMouseUp?.();
26418
+ }
26419
+ isTouchEvent(e) {
26420
+ return "touches" in e;
26421
+ }
26422
+ startDrawingMouse(e) {
26423
+ if (this.isRevealed || this.isAnimating)
26424
+ return;
26425
+ e.preventDefault();
26426
+ this.isDrawing = true;
26427
+ this.handleDrawingMouse(e);
26428
+ }
26429
+ startDrawingTouch(e) {
26430
+ if (this.isRevealed || this.isAnimating)
26431
+ return;
26432
+ e.preventDefault();
26433
+ this.isDrawing = true;
26434
+ this.handleDrawingTouch(e);
26435
+ }
26436
+ handleDrawingMouse(e) {
26437
+ if (!this.isDrawing || this.isRevealed || this.isAnimating)
26438
+ return;
26439
+ e.preventDefault();
26440
+ const { x, y } = this.transformer.toCanvas(e.clientX, e.clientY);
26441
+ this.processScratch(x, y);
26442
+ }
26443
+ handleDrawingTouch(e) {
26444
+ if (!this.isDrawing || this.isRevealed || this.isAnimating)
26445
+ return;
26446
+ e.preventDefault();
26447
+ let x = 0;
26448
+ let y = 0;
26449
+ if (e.touches.length > 0) {
26450
+ const p = this.transformer.toCanvas(e.touches[0].clientX, e.touches[0].clientY);
26451
+ x = p.x;
26452
+ y = p.y;
26453
+ }
26454
+ this.processScratch(x, y);
26455
+ }
26456
+ processScratch(x, y) {
26457
+ const radius = this.options.brushSize;
26458
+ const prev = this.lastScratchX != null && this.lastScratchY != null ? { x: this.lastScratchX, y: this.lastScratchY } : null;
26459
+ const newScratchedPixels = this.brush.apply(x, y, radius, prev);
26460
+ if (this.options.brushType === BrushType.Line) {
26461
+ this.lastScratchX = x;
26462
+ this.lastScratchY = y;
26463
+ }
26464
+ if (newScratchedPixels > 0) {
26465
+ this.handleScratchProgress(newScratchedPixels);
26466
+ }
26467
+ }
26468
+ handleScratchProgress(newScratchedPixels) {
26469
+ this.scratchedPixels += newScratchedPixels;
26470
+ this.renderer.applyMask(this.maskModel.mask, this.maskModel.totalPixels);
26471
+ const percent = this.calculateScratchPercentage();
26472
+ this.options.onProgress?.(Math.floor(percent));
26473
+ if (this.isComplete(percent)) {
26474
+ this.startCompleteReveal();
26475
+ }
26476
+ }
26477
+ isComplete(percent) {
26478
+ return !this.isRevealed && !this.isAnimating && percent >= this.options.scratchThreshold * 100;
26479
+ }
26480
+ calculateScratchPercentage() {
26481
+ if (this.maskModel.inViewportFlags) {
26482
+ if (this.maskModel.viewportPixelTotal <= 0) {
26483
+ return this.maskModel.scratchablePixelTotal <= 0 ? 100 : 0;
26484
+ }
26485
+ return (this.scratchedPixels / this.maskModel.viewportPixelTotal) * 100;
26486
+ }
26487
+ if (this.maskModel.scratchablePixelTotal <= 0) {
26488
+ return 100;
26489
+ }
26490
+ return (this.scratchedPixels / this.maskModel.scratchablePixelTotal) * 100;
26491
+ }
26492
+ startCompleteReveal() {
26493
+ if (this.isRevealed || this.isAnimating)
26494
+ return;
26495
+ this.isRevealed = true;
26496
+ this.isAnimating = true;
26497
+ this.options.onComplete?.();
26498
+ this.completeEffect.startComplete();
26499
+ }
26500
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLElement`, `ScratchLayerOptions`]; }
26501
+ }
26502
+
26503
+ class LocalDataKeys {
26504
+ doneAt;
26505
+ constructor(elementId) {
26506
+ this.doneAt = `_sc_${elementId}_done_at`;
26507
+ }
26508
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`string`]; }
26509
+ }
26510
+ const toPixelFromSlideEmRelativeFactor = 19.5; // 390 / 20; // (390 - slide width) / (20px - show in input)
26511
+ class ScratchCardModel {
26512
+ brushSize;
26513
+ brushType;
26514
+ scratchThreshold;
26515
+ noiseDensity;
26516
+ constructor(element) {
26517
+ this.brushSize = parseFloat(getTagData(element, "brushSize") || "30");
26518
+ this.brushType = getTagData(element, "brushType") ?? BrushType.Line;
26519
+ this.scratchThreshold = parseFloat(getTagData(element, "scratchThreshold") ?? "70") / 100;
26520
+ this.noiseDensity = parseFloat(getTagData(element, "noiseDensity") ?? "100");
26521
+ }
26522
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLElement`]; }
26523
+ }
26524
+ class WidgetScratchCard extends WidgetBase {
26525
+ static DEFAULTS = {
26526
+ slide: null,
26527
+ activateAfterCreate: false,
26528
+ create: false,
26529
+ localData: {},
26530
+ };
26531
+ static widgetClassName = "narrative-element-scratch-card";
26532
+ scratchContainer = null;
26533
+ scratchLayer = null;
26534
+ localDataKeys;
26535
+ model;
26536
+ _isClickCaptured = false;
26537
+ constructor(element, options, widgetCallbacks, widgetDeps) {
26538
+ super(element, options, widgetCallbacks, widgetDeps);
26539
+ this.localDataKeys = new LocalDataKeys(this.elementId);
26540
+ this.model = new ScratchCardModel(this.element);
26541
+ }
26542
+ get isClickCaptured() {
26543
+ return this._isClickCaptured;
26544
+ }
26545
+ onRefreshUserData(localData) {
26546
+ super.onRefreshUserData(localData);
26547
+ this.firstOpenTime = new Date().getTime();
26548
+ if (this.isDone()) {
26549
+ this.hideGeometry();
26550
+ /* this.startReadyPromise.then(() => {
26551
+ if (this.disableTimer) {
26552
+ this.onWidgetComplete();
26553
+ }
26554
+ }); */
26555
+ }
26556
+ }
26557
+ isDone() {
26558
+ return this.localData[this.localDataKeys.doneAt] != null;
26559
+ }
26560
+ onStart() {
26561
+ super.onStart();
26562
+ this.init();
26563
+ this.scratchLayer?.enable();
26564
+ this.env.addEventListener("resize", this.handleResize);
26565
+ }
26566
+ onStop() {
26567
+ super.onStop();
26568
+ this.env.removeEventListener("resize", this.handleResize);
26569
+ if (!this.scratchLayer)
26570
+ return;
26571
+ this.scratchLayer.disable();
26572
+ if (!this.isDone()) {
26573
+ this.scratchLayer.reset();
26574
+ }
26575
+ this._isClickCaptured = false;
26576
+ }
26577
+ init() {
26578
+ if (this.isDone() || this.scratchContainer)
26579
+ return;
26580
+ this.scratchContainer = this.mountScratchContainer();
26581
+ this.renderScratchLayer(this.scratchContainer);
26582
+ }
26583
+ calcBrushSize() {
26584
+ const emBrushSize = this.model.brushSize / toPixelFromSlideEmRelativeFactor;
26585
+ return emBrushSize * (parseFloat(this.widgetDeps.slideRoot.style.fontSize) || toPixelFromSlideEmRelativeFactor);
26586
+ }
26587
+ handleResize = () => {
26588
+ if (!this.scratchLayer)
26589
+ return;
26590
+ this.scratchLayer.updateOptions({
26591
+ brushSize: this.calcBrushSize(),
26592
+ brushType: this.model.brushType,
26593
+ });
26594
+ this.scratchLayer.updateSize();
26595
+ };
26596
+ mountScratchContainer() {
26597
+ const container = document.createElement("div");
26598
+ container.classList.add("narrative-element-scratch-card-container");
26599
+ container.style.width = "100%";
26600
+ container.style.height = "100%";
26601
+ this.element.appendChild(container);
26602
+ return container;
26603
+ }
26604
+ renderScratchLayer(container) {
26605
+ const image = this.getImage();
26606
+ const geometryParent = this.element.parentElement;
26607
+ const angleRaw = geometryParent ? getTagData(geometryParent, "angle") : null;
26608
+ const geometryAngleRad = angleRaw != null && angleRaw !== "" ? parseFloat(angleRaw) : null;
26609
+ this.scratchLayer = new ScratchLayer(container, {
26610
+ backgroundImage: image,
26611
+ imageObjectFit: ScratchImageObjectFit.Cover,
26612
+ brushSize: this.calcBrushSize(),
26613
+ brushType: this.model.brushType,
26614
+ scratchThreshold: this.model.scratchThreshold,
26615
+ texture: { noiseDensity: 0 },
26616
+ mouseContainer: this.widgetDeps.slideRoot,
26617
+ viewportElement: this.slide,
26618
+ progressClipElement: this.element,
26619
+ geometryAngleRad: geometryAngleRad != null && !Number.isNaN(geometryAngleRad) ? geometryAngleRad : null,
26620
+ onMouseDown: () => {
26621
+ this.handleMouseDown();
26622
+ },
26623
+ onMouseUp: () => {
26624
+ this.handleMouseUp();
26625
+ },
26626
+ onComplete: () => {
26627
+ this.completeWidget();
26628
+ },
26629
+ onFadeComplete: () => {
26630
+ this.hideGeometry();
26631
+ },
26632
+ fadeDuration: 300,
26633
+ fadeTimeout: 0,
26634
+ });
26635
+ image.parentElement?.remove();
26636
+ }
26637
+ getImage() {
26638
+ const img = this.element.querySelector("img");
26639
+ if (!img)
26640
+ throw new Error("Scratch image not found");
26641
+ return img;
26642
+ }
26643
+ hideGeometry() {
26644
+ this.element.parentElement.style.display = "none";
26645
+ }
26646
+ completeWidget() {
26647
+ this.disablePointerEventsOnGeometry();
26648
+ this.sendStatEvent();
26649
+ this.saveToLocalData();
26650
+ /* if (this.disableTimer) {
26651
+ this.onWidgetComplete();
26652
+ } */
26653
+ }
26654
+ disablePointerEventsOnGeometry() {
26655
+ this.element.parentElement.style.pointerEvents = "none";
26656
+ }
26657
+ sendStatEvent() {
26658
+ const duration = new Date().getTime() - this.firstOpenTime;
26659
+ this.sendStatisticEventToApp("w-scratch-card-complete", {
26660
+ ...this.statisticEventBaseFieldsShortForm,
26661
+ wi: this.elementId,
26662
+ d: duration,
26663
+ });
26664
+ }
26665
+ saveToLocalData() {
26666
+ const time = Math.round(new Date().getTime() / 1000);
26667
+ this.localData[this.localDataKeys.doneAt] = time;
26668
+ this.setLocalData(this.localData, true);
26669
+ }
26670
+ handleMouseUp() {
26671
+ this.env.requestAnimationFrame(() => {
26672
+ this.widgetDeps.slideApiDeps.enableHorizontalSwipeGesture();
26673
+ this.widgetDeps.slideApiDeps.enableVerticalSwipeGesture();
26674
+ this._isClickCaptured = false;
26675
+ });
26676
+ }
26677
+ handleMouseDown() {
26678
+ this.widgetDeps.slideApiDeps.disableHorizontalSwipeGesture();
26679
+ this.widgetDeps.slideApiDeps.disableVerticalSwipeGesture();
26680
+ this._isClickCaptured = true;
26681
+ if (!this.widgetDeps.slideApiDeps.isSdkSupportVerticalSwipeGestureControl()) {
26682
+ this.scratchLayer?.startCompleteReveal();
26683
+ }
26684
+ }
26685
+ static api = {
26686
+ widgetClassName: WidgetScratchCard.widgetClassName,
26687
+ onRefreshUserData: WidgetScratchCard.onRefreshUserData,
26688
+ init: function (element, localData, widgetCallbacks, widgetDeps) {
26689
+ WidgetScratchCard.initWidget(element, localData, (element, options) => new WidgetScratchCard(element, options, widgetCallbacks, widgetDeps));
26690
+ },
26691
+ onStart: function (element) {
26692
+ WidgetScratchCard.getInstance(element)?.onStart();
26693
+ },
26694
+ onStop: function (element) {
26695
+ WidgetScratchCard.getInstance(element)?.onStop();
26696
+ },
26697
+ isClickCaptured: function (element) {
26698
+ return WidgetScratchCard.getInstance(element)?.isClickCaptured ?? false;
26699
+ },
26700
+ };
26701
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLElement`, `Partial`, `WidgetCallbacks`, `WidgetDeps`]; }
26702
+ }
26703
+
25310
26704
  class EsModuleLayoutApi {
25311
26705
  slideApiPeerDeps;
25312
26706
  constructor(slideApiPeerDeps) {
@@ -25372,6 +26766,9 @@ class EsModuleLayoutApi {
25372
26766
  get widgetReactionsApi() {
25373
26767
  return WidgetReactions.api;
25374
26768
  }
26769
+ get widgetScratchCardApi() {
26770
+ return WidgetScratchCard.api;
26771
+ }
25375
26772
  get VideoPlayer() {
25376
26773
  return this.slideApiPeerDeps().VODPlayer;
25377
26774
  }