@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 +1410 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.js +1410 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
5501
|
-
if (this.activeLayer.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|