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