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