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