@schukai/monster 4.67.0 → 4.69.0

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.
@@ -0,0 +1,3251 @@
1
+ /**
2
+ * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact Volker Schukai.
11
+ *
12
+ * SPDX-License-Identifier: AGPL-3.0
13
+ */
14
+
15
+ import { instanceSymbol } from "../../constants.mjs";
16
+ import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
17
+ import { CustomElement } from "../../dom/customelement.mjs";
18
+ import {
19
+ assembleMethodSymbol,
20
+ registerCustomElement,
21
+ } from "../../dom/customelement.mjs";
22
+ import { ControlStyleSheet } from "../stylesheet/control.mjs";
23
+ import { FormStyleSheet } from "../stylesheet/form.mjs";
24
+ import { SpaceStyleSheet } from "../stylesheet/space.mjs";
25
+ import "../form/button-bar.mjs";
26
+ import "../form/field-set.mjs";
27
+ import "../form/message-state-button.mjs";
28
+ import "../layout/split-panel.mjs";
29
+ import "../state/state.mjs";
30
+ import { getLocaleOfDocument } from "../../dom/locale.mjs";
31
+ import { addErrorAttribute } from "../../dom/error.mjs";
32
+ import { fireCustomEvent } from "../../dom/events.mjs";
33
+ import { isString } from "../../types/is.mjs";
34
+
35
+ export { ImageEditor };
36
+
37
+ const READONLY_ATTRIBUTE = "data-monster-readonly";
38
+
39
+ /**
40
+ * @private
41
+ * @type {symbol}
42
+ */
43
+ const controlElementSymbol = Symbol("controlElement");
44
+
45
+ /**
46
+ * @private
47
+ * @type {symbol}
48
+ */
49
+ const stageElementSymbol = Symbol("stageElement");
50
+
51
+ /**
52
+ * @private
53
+ * @type {symbol}
54
+ */
55
+ const surfaceElementSymbol = Symbol("surfaceElement");
56
+
57
+ /**
58
+ * @private
59
+ * @type {symbol}
60
+ */
61
+ const canvasElementSymbol = Symbol("canvasElement");
62
+
63
+ /**
64
+ * @private
65
+ * @type {symbol}
66
+ */
67
+ const overlayElementSymbol = Symbol("overlayElement");
68
+
69
+ /**
70
+ * @private
71
+ * @type {symbol}
72
+ */
73
+ const filterSelectElementSymbol = Symbol("filterSelectElement");
74
+
75
+ /**
76
+ * @private
77
+ * @type {symbol}
78
+ */
79
+ const filterIntensityElementSymbol = Symbol("filterIntensityElement");
80
+
81
+ /**
82
+ * @private
83
+ * @type {symbol}
84
+ */
85
+ const zoomInputElementSymbol = Symbol("zoomInputElement");
86
+
87
+ /**
88
+ * @private
89
+ * @type {symbol}
90
+ */
91
+ const rotationInputElementSymbol = Symbol("rotationInputElement");
92
+
93
+ /**
94
+ * @private
95
+ * @type {symbol}
96
+ */
97
+ const rotationRangeElementSymbol = Symbol("rotationRangeElement");
98
+
99
+ /**
100
+ * @private
101
+ * @type {symbol}
102
+ */
103
+ const cropInputsElementSymbol = Symbol("cropInputsElement");
104
+
105
+ /**
106
+ * @private
107
+ * @type {symbol}
108
+ */
109
+ const cropInputXElementSymbol = Symbol("cropInputXElement");
110
+
111
+ /**
112
+ * @private
113
+ * @type {symbol}
114
+ */
115
+ const cropInputYElementSymbol = Symbol("cropInputYElement");
116
+
117
+ /**
118
+ * @private
119
+ * @type {symbol}
120
+ */
121
+ const cropInputWidthElementSymbol = Symbol("cropInputWidthElement");
122
+
123
+ /**
124
+ * @private
125
+ * @type {symbol}
126
+ */
127
+ const cropInputHeightElementSymbol = Symbol("cropInputHeightElement");
128
+
129
+ /**
130
+ * @private
131
+ * @type {symbol}
132
+ */
133
+ const viewSectionElementSymbol = Symbol("viewSectionElement");
134
+
135
+ /**
136
+ * @private
137
+ * @type {symbol}
138
+ */
139
+ const selectionSectionElementSymbol = Symbol("selectionSectionElement");
140
+
141
+ /**
142
+ * @private
143
+ * @type {symbol}
144
+ */
145
+ const filterSectionElementSymbol = Symbol("filterSectionElement");
146
+
147
+ /**
148
+ * @private
149
+ * @type {symbol}
150
+ */
151
+ const startContentElementSymbol = Symbol("startContentElement");
152
+
153
+ /**
154
+ * @private
155
+ * @type {symbol}
156
+ */
157
+ const endContentElementSymbol = Symbol("endContentElement");
158
+
159
+ /**
160
+ * @private
161
+ * @type {symbol}
162
+ */
163
+ const selectButtonElementSymbol = Symbol("selectButtonElement");
164
+
165
+ /**
166
+ * @private
167
+ * @type {symbol}
168
+ */
169
+ const applyCropButtonElementSymbol = Symbol("applyCropButtonElement");
170
+
171
+ /**
172
+ * @private
173
+ * @type {symbol}
174
+ */
175
+ const resetButtonElementSymbol = Symbol("resetButtonElement");
176
+
177
+ /**
178
+ * @private
179
+ * @type {symbol}
180
+ */
181
+ const saveButtonElementSymbol = Symbol("saveButtonElement");
182
+
183
+ /**
184
+ * @private
185
+ * @type {symbol}
186
+ */
187
+ const emptyStateElementSymbol = Symbol("emptyStateElement");
188
+
189
+ /**
190
+ * @private
191
+ * @type {symbol}
192
+ */
193
+ const sourceImageSymbol = Symbol("sourceImage");
194
+
195
+ /**
196
+ * @private
197
+ * @type {symbol}
198
+ */
199
+ const originalSourceSymbol = Symbol("originalSource");
200
+
201
+ /**
202
+ * @private
203
+ * @type {symbol}
204
+ */
205
+ const cropStateSymbol = Symbol("cropState");
206
+
207
+ /**
208
+ * @private
209
+ * @type {symbol}
210
+ */
211
+ const autoLoadSymbol = Symbol("autoLoad");
212
+
213
+ /**
214
+ * @private
215
+ * @type {symbol}
216
+ */
217
+ const stageAspectSymbol = Symbol("stageAspect");
218
+
219
+ /**
220
+ * @private
221
+ * @type {symbol}
222
+ */
223
+ const viewStateSymbol = Symbol("viewState");
224
+
225
+ /**
226
+ * @private
227
+ * @type {symbol}
228
+ */
229
+ const stageResizeObserverSymbol = Symbol("stageResizeObserver");
230
+
231
+ /**
232
+ * @private
233
+ * @type {symbol}
234
+ */
235
+ const cropInputsResizeObserverSymbol = Symbol("cropInputsResizeObserver");
236
+
237
+ /**
238
+ * @private
239
+ * @type {symbol}
240
+ */
241
+ const filterApplyButtonElementSymbol = Symbol("filterApplyButtonElement");
242
+
243
+ /**
244
+ * @private
245
+ * @type {symbol}
246
+ */
247
+ const filterRegionsSymbol = Symbol("filterRegions");
248
+
249
+ /**
250
+ * @private
251
+ * @type {symbol}
252
+ */
253
+ const rotationSymbol = Symbol("rotation");
254
+
255
+ /**
256
+ * @private
257
+ * @type {symbol}
258
+ */
259
+ const rotateLeftButtonElementSymbol = Symbol("rotateLeftButtonElement");
260
+
261
+ /**
262
+ * @private
263
+ * @type {symbol}
264
+ */
265
+ const rotateRightButtonElementSymbol = Symbol("rotateRightButtonElement");
266
+
267
+ /**
268
+ * @private
269
+ * @type {symbol}
270
+ */
271
+ const rotateResetButtonElementSymbol = Symbol("rotateResetButtonElement");
272
+
273
+ /**
274
+ * An Image Editor Component
275
+ *
276
+ * @fragments /fragments/components/content/image-editor/
277
+ *
278
+ * @since 4.68.0
279
+ * @copyright Volker Schukai
280
+ * @summary An image editor for cropping and basic filters.
281
+ * @fires monster-image-editor-saved
282
+ */
283
+ class ImageEditor extends CustomElement {
284
+ /**
285
+ * Constructor for the ImageEditor class.
286
+ * Calls the parent class constructor.
287
+ */
288
+ constructor() {
289
+ super();
290
+
291
+ this[cropStateSymbol] = {
292
+ enabled: false,
293
+ active: false,
294
+ mode: "draw",
295
+ startX: 0,
296
+ startY: 0,
297
+ endX: 0,
298
+ endY: 0,
299
+ offsetX: 0,
300
+ offsetY: 0,
301
+ handle: null,
302
+ anchorX: 0,
303
+ anchorY: 0,
304
+ };
305
+
306
+ this[autoLoadSymbol] = false;
307
+ this[stageAspectSymbol] = null;
308
+ this[viewStateSymbol] = {
309
+ scale: 1,
310
+ offsetX: 0,
311
+ offsetY: 0,
312
+ isPanning: false,
313
+ lastX: 0,
314
+ lastY: 0,
315
+ };
316
+ this[stageResizeObserverSymbol] = null;
317
+ this[cropInputsResizeObserverSymbol] = null;
318
+ this[filterRegionsSymbol] = [];
319
+ this[rotationSymbol] = 0;
320
+ }
321
+
322
+ /**
323
+ * This method is called by the `instanceof` operator.
324
+ * @return {symbol}
325
+ */
326
+ static get [instanceSymbol]() {
327
+ return Symbol.for(
328
+ "@schukai/monster/components/content/image-editor@instance",
329
+ );
330
+ }
331
+
332
+ /**
333
+ *
334
+ * @return {Components.Content.ImageEditor
335
+ */
336
+ [assembleMethodSymbol]() {
337
+ super[assembleMethodSymbol]();
338
+ initControlReferences.call(this);
339
+ initEventHandler.call(this);
340
+ applyFeatureFlags.call(this);
341
+ syncFilterControls.call(this);
342
+ setupStageResizeObserver.call(this);
343
+ setupCropInputsResizeObserver.call(this);
344
+ updateUiState.call(this, false);
345
+ return this;
346
+ }
347
+
348
+ /**
349
+ * @return {void}
350
+ */
351
+ connectedCallback() {
352
+ super.connectedCallback();
353
+ setupInitialSource.call(this);
354
+ applyReadOnlyState.call(this);
355
+ }
356
+
357
+ /**
358
+ * @param {string} name
359
+ * @param {string|null} oldValue
360
+ * @param {string|null} newValue
361
+ */
362
+ attributeChangedCallback(name, oldValue, newValue) {
363
+ super.attributeChangedCallback(name, oldValue, newValue);
364
+ if (name === READONLY_ATTRIBUTE && oldValue !== newValue) {
365
+ applyReadOnlyState.call(this);
366
+ updateUiState.call(this, Boolean(this[sourceImageSymbol]));
367
+ }
368
+ }
369
+
370
+ /**
371
+ * @return {void}
372
+ */
373
+ disconnectedCallback() {
374
+ super.disconnectedCallback();
375
+ if (this[stageResizeObserverSymbol]) {
376
+ this[stageResizeObserverSymbol].disconnect();
377
+ this[stageResizeObserverSymbol] = null;
378
+ }
379
+ if (this[cropInputsResizeObserverSymbol]) {
380
+ this[cropInputsResizeObserverSymbol].disconnect();
381
+ this[cropInputsResizeObserverSymbol] = null;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * To set the options via the HTML Tag, the attribute `data-monster-options` must be used.
387
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
388
+ *
389
+ * @property {Object} templates Template definitions
390
+ * @property {string} templates.main Main template
391
+ * @property {Object} source Source configuration
392
+ * @property {string|null} source.url URL to load an image from
393
+ * @property {*} source.data Binary data to load an image from
394
+ * @property {string} source.contentType Content type for binary data
395
+ * @property {Object} features Feature configuration
396
+ * @property {boolean} features.allowCrop=true Enable crop tools
397
+ * @property {boolean} features.allowFilters=true Enable filter tools
398
+ * @property {boolean} features.fetchUrl=true Fetch URLs as blobs before loading
399
+ * @property {boolean} features.crossOrigin=true Set crossOrigin for URLs
400
+ * @property {Object} output Output configuration
401
+ * @property {string} output.type="image/png" Output MIME type
402
+ * @property {number} output.quality=0.92 Output quality for lossy formats
403
+ * @property {Object} labels Labels
404
+ */
405
+ get defaults() {
406
+ return Object.assign({}, super.defaults, {
407
+ templates: {
408
+ main: getTemplate(),
409
+ },
410
+ source: {
411
+ url: null,
412
+ data: null,
413
+ contentType: "image/png",
414
+ },
415
+ features: {
416
+ allowCrop: true,
417
+ allowFilters: true,
418
+ fetchUrl: true,
419
+ crossOrigin: true,
420
+ },
421
+ output: {
422
+ type: "image/png",
423
+ quality: 0.92,
424
+ },
425
+ labels: getTranslations(),
426
+ });
427
+ }
428
+
429
+ /**
430
+ * @return {string}
431
+ */
432
+ static getTag() {
433
+ return "monster-image-editor";
434
+ }
435
+
436
+ /**
437
+ * @return {string[]}
438
+ */
439
+ static get observedAttributes() {
440
+ const attributes = super.observedAttributes;
441
+ attributes.push(READONLY_ATTRIBUTE);
442
+ return attributes;
443
+ }
444
+
445
+ /**
446
+ * @return {CSSStyleSheet[]}
447
+ */
448
+ static getCSSStyleSheet() {
449
+ return [ControlStyleSheet, FormStyleSheet, SpaceStyleSheet];
450
+ }
451
+
452
+ /**
453
+ * Sets the image source from binary data or a URL.
454
+ *
455
+ * @param {(Blob|ArrayBuffer|Uint8Array|string)} data
456
+ * @param {Object} [options]
457
+ * @param {string} [options.contentType]
458
+ * @param {boolean} [options.storeOriginal]
459
+ * @return {Promise<void>}
460
+ */
461
+ setImage(data, options = {}) {
462
+ return setImageFromData.call(this, data, options);
463
+ }
464
+
465
+ /**
466
+ * Load an image from a URL.
467
+ * @param {string} url
468
+ * @return {Promise<void>}
469
+ */
470
+ load(url) {
471
+ return loadFromUrl.call(this, url);
472
+ }
473
+
474
+ /**
475
+ * Reset the editor to the original image and clear edits.
476
+ * @return {Promise<void>|void}
477
+ */
478
+ reset() {
479
+ return resetEditor.call(this);
480
+ }
481
+
482
+ /**
483
+ * Save the current image and emit a save event.
484
+ * @return {Promise<Blob|null>}
485
+ */
486
+ save() {
487
+ return saveImage.call(this);
488
+ }
489
+
490
+ /**
491
+ * Returns a blob for the current image state.
492
+ * @param {string} [type]
493
+ * @param {number} [quality]
494
+ * @return {Promise<Blob|null>}
495
+ */
496
+ getImageBlob(type, quality) {
497
+ return getImageBlob.call(this, type, quality);
498
+ }
499
+
500
+ /**
501
+ * Returns a data URL for the current image state.
502
+ * @param {string} [type]
503
+ * @param {number} [quality]
504
+ * @return {string|null}
505
+ */
506
+ getImageDataUrl(type, quality) {
507
+ return getImageDataUrl.call(this, type, quality);
508
+ }
509
+
510
+ /**
511
+ * Adds a custom action button into the toolbar.
512
+ * @param {{label?:string,onClick?:Function,classes?:string}} options
513
+ * @return {HTMLElement}
514
+ */
515
+ addActionButton(options = {}) {
516
+ const button = document.createElement("monster-message-state-button");
517
+ button.setAttribute("slot", "actions");
518
+
519
+ this.appendChild(button);
520
+
521
+ queueMicrotask(() => {
522
+ if (options.label && button.setOption) {
523
+ button.setOption("labels.button", options.label);
524
+ }
525
+ if (options.classes && button.setOption) {
526
+ button.setOption("classes.button", options.classes);
527
+ }
528
+ if (options.onClick && button.setOption) {
529
+ button.setOption("actions.click", options.onClick);
530
+ }
531
+ });
532
+
533
+ return button;
534
+ }
535
+ }
536
+
537
+ /**
538
+ * @private
539
+ */
540
+ function setupInitialSource() {
541
+ if (this[autoLoadSymbol]) {
542
+ return;
543
+ }
544
+
545
+ this[autoLoadSymbol] = true;
546
+
547
+ const data = this.getOption("source.data");
548
+ const url = this.getOption("source.url");
549
+
550
+ if (data) {
551
+ setImageFromData.call(this, data, {
552
+ contentType: this.getOption("source.contentType"),
553
+ storeOriginal: true,
554
+ });
555
+ return;
556
+ }
557
+
558
+ if (url) {
559
+ loadFromUrl.call(this, url, { storeOriginal: true });
560
+ }
561
+ }
562
+
563
+ /**
564
+ * @private
565
+ * @return {void}
566
+ */
567
+ function initControlReferences() {
568
+ this[controlElementSymbol] = this.shadowRoot.querySelector(
569
+ `[${ATTRIBUTE_ROLE}="control"]`,
570
+ );
571
+ this[controlElementSymbol].style.height = "100%";
572
+ this[controlElementSymbol].style.minHeight = "0";
573
+ this[controlElementSymbol].style.display = "flex";
574
+ this[controlElementSymbol].style.flexDirection = "column";
575
+ this.style.height = "100%";
576
+ this.style.minHeight = "0";
577
+
578
+ const splitPanel = this.shadowRoot.querySelector(
579
+ `[data-monster-role="splitPanel"]`,
580
+ );
581
+ if (splitPanel) {
582
+ splitPanel.style.flex = "1";
583
+ splitPanel.style.height = "100%";
584
+ splitPanel.style.minHeight = "0";
585
+ const splitShadow = splitPanel.shadowRoot;
586
+ const startPanel = splitShadow?.querySelector(
587
+ `[data-monster-role="startPanel"]`,
588
+ );
589
+ if (startPanel) {
590
+ startPanel.style.minHeight = "0";
591
+ startPanel.style.height = "100%";
592
+ startPanel.style.overflowY = "auto";
593
+ startPanel.style.overflowX = "hidden";
594
+ }
595
+ const endPanel = splitShadow?.querySelector(
596
+ `[data-monster-role="endPanel"]`,
597
+ );
598
+ if (endPanel) {
599
+ endPanel.style.minHeight = "0";
600
+ endPanel.style.height = "100%";
601
+ endPanel.style.overflowY = "auto";
602
+ endPanel.style.overflowX = "hidden";
603
+ }
604
+ }
605
+
606
+ this[stageElementSymbol] = this.shadowRoot.querySelector(
607
+ `[data-monster-role="stage"]`,
608
+ );
609
+
610
+ this[canvasElementSymbol] = this.shadowRoot.querySelector(
611
+ `[data-monster-role="canvas"]`,
612
+ );
613
+
614
+ this[surfaceElementSymbol] = this.shadowRoot.querySelector(
615
+ `[data-monster-role="surface"]`,
616
+ );
617
+
618
+ this[overlayElementSymbol] = this.shadowRoot.querySelector(
619
+ `[data-monster-role="overlay"]`,
620
+ );
621
+
622
+ this[stageElementSymbol].style.position = "relative";
623
+ this[stageElementSymbol].style.width = "100%";
624
+ this[stageElementSymbol].style.overflow = "hidden";
625
+ this[stageElementSymbol].style.touchAction = "none";
626
+
627
+ this[surfaceElementSymbol].style.position = "relative";
628
+ this[surfaceElementSymbol].style.width = "100%";
629
+ this[surfaceElementSymbol].style.height = "100%";
630
+
631
+ this[startContentElementSymbol] = this.shadowRoot.querySelector(
632
+ `[data-monster-role="startContent"]`,
633
+ );
634
+ if (this[startContentElementSymbol]) {
635
+ this[startContentElementSymbol].style.overflow = "auto";
636
+ this[startContentElementSymbol].style.maxHeight = "100%";
637
+ this[startContentElementSymbol].style.minHeight = "0";
638
+ this[startContentElementSymbol].style.height = "100%";
639
+ this[startContentElementSymbol].style.boxSizing = "border-box";
640
+ }
641
+
642
+ this[endContentElementSymbol] = this.shadowRoot.querySelector(
643
+ `[data-monster-role="endContent"]`,
644
+ );
645
+ if (this[endContentElementSymbol]) {
646
+ this[endContentElementSymbol].style.overflow = "auto";
647
+ this[endContentElementSymbol].style.maxHeight = "100%";
648
+ this[endContentElementSymbol].style.minHeight = "0";
649
+ this[endContentElementSymbol].style.height = "100%";
650
+ this[endContentElementSymbol].style.boxSizing = "border-box";
651
+ }
652
+
653
+ this[canvasElementSymbol].style.display = "block";
654
+ this[canvasElementSymbol].style.width = "100%";
655
+ this[canvasElementSymbol].style.height = "100%";
656
+
657
+ this[overlayElementSymbol].style.position = "absolute";
658
+ this[overlayElementSymbol].style.left = "0";
659
+ this[overlayElementSymbol].style.top = "0";
660
+ this[overlayElementSymbol].style.width = "100%";
661
+ this[overlayElementSymbol].style.height = "100%";
662
+ this[overlayElementSymbol].style.pointerEvents = "none";
663
+
664
+ this[filterSelectElementSymbol] = this.shadowRoot.querySelector(
665
+ `[data-monster-role="filterSelect"]`,
666
+ );
667
+
668
+ this[filterIntensityElementSymbol] = this.shadowRoot.querySelector(
669
+ `[data-monster-role="filterIntensity"]`,
670
+ );
671
+
672
+ this[zoomInputElementSymbol] = this.shadowRoot.querySelector(
673
+ `[data-monster-role="zoomInput"]`,
674
+ );
675
+
676
+ this[rotationInputElementSymbol] = this.shadowRoot.querySelector(
677
+ `[data-monster-role="rotationInput"]`,
678
+ );
679
+
680
+ this[rotationRangeElementSymbol] = this.shadowRoot.querySelector(
681
+ `[data-monster-role="rotationRange"]`,
682
+ );
683
+
684
+ this[viewSectionElementSymbol] = this.shadowRoot.querySelector(
685
+ `[data-monster-role="viewSection"]`,
686
+ );
687
+
688
+ this[selectionSectionElementSymbol] = this.shadowRoot.querySelector(
689
+ `[data-monster-role="selectionSection"]`,
690
+ );
691
+
692
+ this[filterSectionElementSymbol] = this.shadowRoot.querySelector(
693
+ `[data-monster-role="filterSection"]`,
694
+ );
695
+
696
+ this[cropInputsElementSymbol] = this.shadowRoot.querySelector(
697
+ `[data-monster-role="cropInputs"]`,
698
+ );
699
+ this[cropInputsElementSymbol].hidden = true;
700
+ this[cropInputsElementSymbol].style.display = "grid";
701
+ this[cropInputsElementSymbol].style.gridTemplateColumns =
702
+ "repeat(2, minmax(0, 1fr))";
703
+ this[cropInputsElementSymbol].style.columnGap = "var(--monster-space-2)";
704
+ this[cropInputsElementSymbol].style.rowGap = "var(--monster-space-2)";
705
+
706
+ this[cropInputXElementSymbol] = this.shadowRoot.querySelector(
707
+ `[data-monster-role="cropInputX"]`,
708
+ );
709
+
710
+ this[cropInputYElementSymbol] = this.shadowRoot.querySelector(
711
+ `[data-monster-role="cropInputY"]`,
712
+ );
713
+
714
+ this[cropInputWidthElementSymbol] = this.shadowRoot.querySelector(
715
+ `[data-monster-role="cropInputWidth"]`,
716
+ );
717
+
718
+ this[cropInputHeightElementSymbol] = this.shadowRoot.querySelector(
719
+ `[data-monster-role="cropInputHeight"]`,
720
+ );
721
+
722
+ this[selectButtonElementSymbol] = this.shadowRoot.querySelector(
723
+ `[data-monster-role="select"]`,
724
+ );
725
+ this[selectButtonElementSymbol].style.width = "100%";
726
+
727
+ this[applyCropButtonElementSymbol] = this.shadowRoot.querySelector(
728
+ `[data-monster-role="applyCrop"]`,
729
+ );
730
+ this[applyCropButtonElementSymbol].hidden = true;
731
+ this[applyCropButtonElementSymbol].style.display = "none";
732
+
733
+ this[filterApplyButtonElementSymbol] = this.shadowRoot.querySelector(
734
+ `[data-monster-role="applyFilter"]`,
735
+ );
736
+ this[filterApplyButtonElementSymbol].style.width = "100%";
737
+
738
+ const topActions = this.shadowRoot.querySelector(
739
+ `[data-monster-role="topActions"]`,
740
+ );
741
+ if (topActions) {
742
+ topActions.style.paddingLeft = "var(--monster-space-5)";
743
+ topActions.style.paddingBottom = "var(--monster-space-4)";
744
+ }
745
+
746
+ this[rotateLeftButtonElementSymbol] = this.shadowRoot.querySelector(
747
+ `[data-monster-role="rotateLeft"]`,
748
+ );
749
+
750
+ this[rotateRightButtonElementSymbol] = this.shadowRoot.querySelector(
751
+ `[data-monster-role="rotateRight"]`,
752
+ );
753
+
754
+ this[rotateResetButtonElementSymbol] = this.shadowRoot.querySelector(
755
+ `[data-monster-role="rotateReset"]`,
756
+ );
757
+
758
+ this[resetButtonElementSymbol] = this.shadowRoot.querySelector(
759
+ `[data-monster-role="reset"]`,
760
+ );
761
+
762
+ this[saveButtonElementSymbol] = this.shadowRoot.querySelector(
763
+ `[data-monster-role="save"]`,
764
+ );
765
+
766
+ this[emptyStateElementSymbol] = this.shadowRoot.querySelector(
767
+ `[data-monster-role="emptyState"]`,
768
+ );
769
+ }
770
+
771
+ /**
772
+ * @private
773
+ * @return {void}
774
+ */
775
+ function initEventHandler() {
776
+ const self = this;
777
+
778
+ this[filterSelectElementSymbol].addEventListener("change", () => {
779
+ syncFilterControls.call(self);
780
+ renderImage.call(self);
781
+ });
782
+
783
+ this[filterIntensityElementSymbol].addEventListener("input", () => {
784
+ renderImage.call(self);
785
+ });
786
+
787
+ this[zoomInputElementSymbol].addEventListener("input", () => {
788
+ const value = Number.parseFloat(
789
+ this[zoomInputElementSymbol].value || "100",
790
+ );
791
+ setViewScale.call(self, value / 100);
792
+ renderImage.call(self);
793
+ });
794
+
795
+ this[rotationInputElementSymbol].addEventListener("input", () => {
796
+ const value = Number.parseFloat(
797
+ this[rotationInputElementSymbol].value || "0",
798
+ );
799
+ setRotation.call(self, value);
800
+ });
801
+
802
+ this[rotationRangeElementSymbol].addEventListener("input", () => {
803
+ const value = Number.parseFloat(
804
+ this[rotationRangeElementSymbol].value || "0",
805
+ );
806
+ setRotation.call(self, value);
807
+ });
808
+
809
+ this[selectButtonElementSymbol].setOption("actions.click", function () {
810
+ handleSelectionButtonClick.call(self);
811
+ });
812
+
813
+ this[applyCropButtonElementSymbol].setOption("actions.click", function () {
814
+ handleCropButtonClick.call(self);
815
+ });
816
+
817
+ this[filterApplyButtonElementSymbol].setOption("actions.click", function () {
818
+ handleFilterApplyClick.call(self);
819
+ });
820
+
821
+ this[resetButtonElementSymbol].setOption("actions.click", function () {
822
+ resetEditor.call(self);
823
+ });
824
+
825
+ this[saveButtonElementSymbol].setOption("actions.click", function () {
826
+ saveImage.call(self);
827
+ });
828
+
829
+ this[rotateLeftButtonElementSymbol].setOption("actions.click", function () {
830
+ rotateImage.call(self, -90);
831
+ });
832
+
833
+ this[rotateRightButtonElementSymbol].setOption("actions.click", function () {
834
+ rotateImage.call(self, 90);
835
+ });
836
+
837
+ this[rotateResetButtonElementSymbol].setOption("actions.click", function () {
838
+ setRotation.call(self, 0);
839
+ });
840
+
841
+ this[overlayElementSymbol].addEventListener("pointerdown", (event) => {
842
+ startCropSelection.call(self, event);
843
+ });
844
+
845
+ this[overlayElementSymbol].addEventListener("pointermove", (event) => {
846
+ updateCropSelection.call(self, event);
847
+ });
848
+
849
+ this[overlayElementSymbol].addEventListener("pointerup", (event) => {
850
+ finishCropSelection.call(self, event);
851
+ });
852
+
853
+ this[overlayElementSymbol].addEventListener("pointerleave", (event) => {
854
+ finishCropSelection.call(self, event);
855
+ });
856
+
857
+ this[stageElementSymbol].addEventListener("pointerdown", (event) => {
858
+ startPan.call(self, event);
859
+ });
860
+
861
+ this[stageElementSymbol].addEventListener("pointermove", (event) => {
862
+ updatePan.call(self, event);
863
+ });
864
+
865
+ this[stageElementSymbol].addEventListener("pointerup", (event) => {
866
+ stopPan.call(self, event);
867
+ });
868
+
869
+ this[stageElementSymbol].addEventListener("pointerleave", (event) => {
870
+ stopPan.call(self, event);
871
+ });
872
+
873
+ for (const input of [
874
+ this[cropInputXElementSymbol],
875
+ this[cropInputYElementSymbol],
876
+ this[cropInputWidthElementSymbol],
877
+ this[cropInputHeightElementSymbol],
878
+ ]) {
879
+ input.addEventListener("input", () => {
880
+ applyCropInputs.call(self);
881
+ });
882
+ }
883
+ }
884
+
885
+ /**
886
+ * @private
887
+ */
888
+ function applyFeatureFlags() {
889
+ const allowFilters = this.getOption("features.allowFilters") !== false;
890
+ const allowCrop = this.getOption("features.allowCrop") !== false;
891
+ const allowSelection = allowCrop || allowFilters;
892
+
893
+ if (!allowFilters) {
894
+ if (this[filterSectionElementSymbol]) {
895
+ this[filterSectionElementSymbol].hidden = true;
896
+ }
897
+ this[filterSelectElementSymbol].disabled = true;
898
+ this[filterIntensityElementSymbol].disabled = true;
899
+ this[filterApplyButtonElementSymbol].disabled = true;
900
+ }
901
+
902
+ if (!allowSelection) {
903
+ this[selectButtonElementSymbol].hidden = true;
904
+ this[applyCropButtonElementSymbol].hidden = true;
905
+ this[overlayElementSymbol].style.pointerEvents = "none";
906
+ this[cropInputsElementSymbol].hidden = true;
907
+ if (this[selectionSectionElementSymbol]) {
908
+ this[selectionSectionElementSymbol].hidden = true;
909
+ }
910
+ }
911
+
912
+ if (!allowCrop) {
913
+ this[applyCropButtonElementSymbol].hidden = true;
914
+ }
915
+ }
916
+
917
+ /**
918
+ * @private
919
+ */
920
+ function isReadOnly() {
921
+ return this.hasAttribute(READONLY_ATTRIBUTE);
922
+ }
923
+
924
+ /**
925
+ * @private
926
+ */
927
+ function applyReadOnlyState() {
928
+ const readOnly = isReadOnly.call(this);
929
+ if (!readOnly) {
930
+ return;
931
+ }
932
+
933
+ if (this[cropStateSymbol].enabled) {
934
+ setCropMode.call(this, false);
935
+ }
936
+ this[overlayElementSymbol].style.pointerEvents = "none";
937
+ }
938
+
939
+ /**
940
+ * @private
941
+ */
942
+ function setupStageResizeObserver() {
943
+ if (this[stageResizeObserverSymbol]) {
944
+ return;
945
+ }
946
+
947
+ this[stageResizeObserverSymbol] = new ResizeObserver(() => {
948
+ updateStageSize.call(this);
949
+ });
950
+
951
+ const container = this[endContentElementSymbol] || this[stageElementSymbol];
952
+ if (container) {
953
+ this[stageResizeObserverSymbol].observe(container);
954
+ }
955
+ }
956
+
957
+ /**
958
+ * @private
959
+ */
960
+ function setupCropInputsResizeObserver() {
961
+ if (this[cropInputsResizeObserverSymbol]) {
962
+ return;
963
+ }
964
+
965
+ this[cropInputsResizeObserverSymbol] = new ResizeObserver(() => {
966
+ updateCropInputsLayout.call(this);
967
+ });
968
+
969
+ this[cropInputsResizeObserverSymbol].observe(this[cropInputsElementSymbol]);
970
+ updateCropInputsLayout.call(this);
971
+ }
972
+
973
+ /**
974
+ * @private
975
+ */
976
+ function updateCropInputsLayout() {
977
+ if (!this[cropInputsElementSymbol]) {
978
+ return;
979
+ }
980
+
981
+ const width = this[cropInputsElementSymbol].clientWidth;
982
+ if (!width) {
983
+ return;
984
+ }
985
+
986
+ this[cropInputsElementSymbol].style.gridTemplateColumns =
987
+ width < 260 ? "minmax(0, 1fr)" : "repeat(2, minmax(0, 1fr))";
988
+ updateCropLabelWidths.call(this);
989
+ }
990
+
991
+ /**
992
+ * @private
993
+ */
994
+ function updateCropLabelWidths() {
995
+ const spans = Array.from(
996
+ this[cropInputsElementSymbol].querySelectorAll("label > span"),
997
+ );
998
+ if (spans.length === 0) {
999
+ return;
1000
+ }
1001
+
1002
+ let maxWidth = 0;
1003
+ for (const span of spans) {
1004
+ span.style.display = "inline-block";
1005
+ span.style.width = "auto";
1006
+ maxWidth = Math.max(maxWidth, span.getBoundingClientRect().width);
1007
+ }
1008
+
1009
+ const targetWidth = Math.ceil(maxWidth);
1010
+ for (const span of spans) {
1011
+ span.style.width = `${targetWidth}px`;
1012
+ }
1013
+ }
1014
+
1015
+ /**
1016
+ * @private
1017
+ */
1018
+ function updateStageSize() {
1019
+ if (!this[stageAspectSymbol]) {
1020
+ this[stageElementSymbol].style.height = "auto";
1021
+ this[stageElementSymbol].style.width = "100%";
1022
+ this[stageElementSymbol].style.margin = "0";
1023
+ return;
1024
+ }
1025
+
1026
+ const container =
1027
+ this[endContentElementSymbol] || this[stageElementSymbol].parentElement;
1028
+ const availableWidth = container?.clientWidth ?? 0;
1029
+ const availableHeight = container?.clientHeight ?? 0;
1030
+ if (!availableWidth) {
1031
+ return;
1032
+ }
1033
+
1034
+ let targetWidth = availableWidth;
1035
+ let targetHeight = Math.round(targetWidth / this[stageAspectSymbol]);
1036
+ if (availableHeight && targetHeight > availableHeight) {
1037
+ targetHeight = availableHeight;
1038
+ targetWidth = Math.round(targetHeight * this[stageAspectSymbol]);
1039
+ }
1040
+
1041
+ this[stageElementSymbol].style.width = `${targetWidth}px`;
1042
+ this[stageElementSymbol].style.height = `${targetHeight}px`;
1043
+ this[stageElementSymbol].style.margin = "0 auto";
1044
+ }
1045
+
1046
+ /**
1047
+ * @private
1048
+ */
1049
+ function updateStageAspectFromRenderSize() {
1050
+ const { width, height } = getRenderSize.call(this);
1051
+ this[stageAspectSymbol] = width > 0 && height > 0 ? width / height : null;
1052
+ updateStageSize.call(this);
1053
+ }
1054
+
1055
+ /**
1056
+ * @private
1057
+ * @param {string} url
1058
+ * @return {Promise<void>}
1059
+ */
1060
+ function loadFromUrl(url, options = {}) {
1061
+ if (!isString(url) || url.length === 0) {
1062
+ addErrorAttribute(this, "Invalid URL");
1063
+ return Promise.reject(new Error("Invalid URL"));
1064
+ }
1065
+
1066
+ if (this.getOption("features.fetchUrl") === false) {
1067
+ return loadImageFromSource.call(this, url, {
1068
+ storeOriginal: options.storeOriginal !== false,
1069
+ resetAspect: options.storeOriginal !== false,
1070
+ resetView: options.storeOriginal !== false,
1071
+ });
1072
+ }
1073
+
1074
+ return fetch(url)
1075
+ .then((response) => {
1076
+ if (!response.ok) {
1077
+ throw new Error(`Failed to fetch image: ${response.status}`);
1078
+ }
1079
+ return response.blob();
1080
+ })
1081
+ .then((blob) => blobToDataUrl(blob))
1082
+ .then((dataUrl) => {
1083
+ return loadImageFromSource.call(this, dataUrl, {
1084
+ storeOriginal: options.storeOriginal !== false,
1085
+ resetAspect: options.storeOriginal !== false,
1086
+ resetView: options.storeOriginal !== false,
1087
+ });
1088
+ })
1089
+ .catch((error) => {
1090
+ addErrorAttribute(this, error.message || error);
1091
+ throw error;
1092
+ });
1093
+ }
1094
+
1095
+ /**
1096
+ * @private
1097
+ * @param {(Blob|ArrayBuffer|Uint8Array|string)} data
1098
+ * @param {Object} options
1099
+ * @return {Promise<void>}
1100
+ */
1101
+ function setImageFromData(data, options) {
1102
+ const contentType =
1103
+ options.contentType || this.getOption("source.contentType");
1104
+ const storeOriginal = options.storeOriginal !== false;
1105
+
1106
+ if (data instanceof Blob) {
1107
+ return blobToDataUrl(data).then((dataUrl) => {
1108
+ return loadImageFromSource.call(this, dataUrl, {
1109
+ storeOriginal,
1110
+ resetAspect: storeOriginal,
1111
+ resetView: storeOriginal,
1112
+ });
1113
+ });
1114
+ }
1115
+
1116
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
1117
+ const blob = new Blob([data], { type: contentType });
1118
+ return blobToDataUrl(blob).then((dataUrl) => {
1119
+ return loadImageFromSource.call(this, dataUrl, {
1120
+ storeOriginal,
1121
+ resetAspect: storeOriginal,
1122
+ resetView: storeOriginal,
1123
+ });
1124
+ });
1125
+ }
1126
+
1127
+ if (isString(data)) {
1128
+ const trimmed = data.trim();
1129
+ if (trimmed.startsWith("data:") || trimmed.startsWith("blob:")) {
1130
+ return loadImageFromSource.call(this, trimmed, {
1131
+ storeOriginal,
1132
+ resetAspect: storeOriginal,
1133
+ resetView: storeOriginal,
1134
+ });
1135
+ }
1136
+
1137
+ if (isURL(trimmed)) {
1138
+ return loadFromUrl.call(this, trimmed, { storeOriginal });
1139
+ }
1140
+
1141
+ const dataUrl = `data:${contentType};base64,${trimmed}`;
1142
+ return loadImageFromSource.call(this, dataUrl, {
1143
+ storeOriginal,
1144
+ resetAspect: storeOriginal,
1145
+ resetView: storeOriginal,
1146
+ });
1147
+ }
1148
+
1149
+ addErrorAttribute(this, "Unsupported image data format");
1150
+ return Promise.reject(new Error("Unsupported image data format"));
1151
+ }
1152
+
1153
+ /**
1154
+ * @private
1155
+ * @param {string} source
1156
+ * @param {Object} options
1157
+ * @return {Promise<void>}
1158
+ */
1159
+ function loadImageFromSource(source, options = {}) {
1160
+ return new Promise((resolve, reject) => {
1161
+ const image = new Image();
1162
+
1163
+ if (this.getOption("features.crossOrigin") !== false) {
1164
+ image.crossOrigin = "anonymous";
1165
+ }
1166
+
1167
+ image.onload = () => {
1168
+ this[sourceImageSymbol] = image;
1169
+ if (options.storeOriginal) {
1170
+ this[originalSourceSymbol] = source;
1171
+ }
1172
+
1173
+ if (options.resetAspect === true || this[stageAspectSymbol] === null) {
1174
+ updateStageAspectFromRenderSize.call(this);
1175
+ }
1176
+
1177
+ resetEditorState.call(this, {
1178
+ keepOriginal: true,
1179
+ preserveFilters: options.preserveFilters === true,
1180
+ });
1181
+ if (options.resetView !== false) {
1182
+ resetViewState.call(this);
1183
+ }
1184
+ renderImage.call(this);
1185
+ setCropMode.call(this, false);
1186
+ updateUiState.call(this, true);
1187
+ resolve();
1188
+ };
1189
+
1190
+ image.onerror = (event) => {
1191
+ addErrorAttribute(this, "Image loading failed");
1192
+ reject(event);
1193
+ };
1194
+
1195
+ image.src = source;
1196
+ });
1197
+ }
1198
+
1199
+ /**
1200
+ * @private
1201
+ * @return {void}
1202
+ */
1203
+ function renderImage() {
1204
+ const image = this[sourceImageSymbol];
1205
+
1206
+ if (!image) {
1207
+ updateUiState.call(this, false);
1208
+ return;
1209
+ }
1210
+
1211
+ const canvas = this[canvasElementSymbol];
1212
+ const overlay = this[overlayElementSymbol];
1213
+ const { width, height } = getRenderSize.call(this);
1214
+ const selectionRect = this[cropStateSymbol].enabled
1215
+ ? getCropRect.call(this, { minSize: 1 })
1216
+ : null;
1217
+
1218
+ canvas.width = width;
1219
+ canvas.height = height;
1220
+ overlay.width = width;
1221
+ overlay.height = height;
1222
+
1223
+ constrainView.call(this);
1224
+
1225
+ const ctx = canvas.getContext("2d");
1226
+ resetCanvasTransform(ctx);
1227
+ ctx.clearRect(0, 0, width, height);
1228
+ drawBaseImage.call(this, ctx, image, { useViewTransform: true });
1229
+
1230
+ if (this[filterRegionsSymbol].length > 0) {
1231
+ for (const region of this[filterRegionsSymbol]) {
1232
+ drawFilteredRegion.call(this, ctx, image, region, {
1233
+ useViewTransform: true,
1234
+ });
1235
+ }
1236
+ }
1237
+
1238
+ drawCropOverlay.call(this);
1239
+ }
1240
+
1241
+ /**
1242
+ * @private
1243
+ * @return {string}
1244
+ */
1245
+ function buildFilterString() {
1246
+ const filter = this[filterSelectElementSymbol].value;
1247
+ const intensityValue = Number.parseFloat(
1248
+ this[filterIntensityElementSymbol].value,
1249
+ );
1250
+ const ratio = Number.isNaN(intensityValue) ? 1 : intensityValue / 100;
1251
+
1252
+ switch (filter) {
1253
+ case "grayscale":
1254
+ return `grayscale(${ratio})`;
1255
+ case "sepia":
1256
+ return `sepia(${ratio})`;
1257
+ case "contrast":
1258
+ return `contrast(${1 + ratio})`;
1259
+ case "saturate":
1260
+ return `saturate(${1 + ratio})`;
1261
+ case "invert":
1262
+ return `invert(${ratio})`;
1263
+ case "blur":
1264
+ return `blur(${Math.max(0, Math.round(ratio * 6))}px)`;
1265
+ case "none":
1266
+ default:
1267
+ return "none";
1268
+ }
1269
+ }
1270
+
1271
+ /**
1272
+ * @private
1273
+ */
1274
+ function syncFilterControls() {
1275
+ const hasImage = Boolean(this[sourceImageSymbol]);
1276
+ const allowFilters = this.getOption("features.allowFilters") !== false;
1277
+ const readOnly = isReadOnly.call(this);
1278
+ const filter = this[filterSelectElementSymbol].value;
1279
+ const disabled = filter === "none";
1280
+ const controlsDisabled = !hasImage || !allowFilters || readOnly;
1281
+ this[filterIntensityElementSymbol].disabled = controlsDisabled || disabled;
1282
+ this[filterApplyButtonElementSymbol].setOption(
1283
+ "disabled",
1284
+ controlsDisabled || disabled,
1285
+ );
1286
+ }
1287
+
1288
+ /**
1289
+ * @private
1290
+ */
1291
+ function handleFilterApplyClick() {
1292
+ if (isReadOnly.call(this)) {
1293
+ return;
1294
+ }
1295
+
1296
+ if (this.getOption("features.allowFilters") === false) {
1297
+ return;
1298
+ }
1299
+
1300
+ if (!this[sourceImageSymbol]) {
1301
+ return;
1302
+ }
1303
+
1304
+ const filter = buildFilterString.call(this);
1305
+ if (filter === "none") {
1306
+ return;
1307
+ }
1308
+
1309
+ const rect = this[cropStateSymbol].enabled
1310
+ ? getCropRect.call(this, { minSize: 1 })
1311
+ : null;
1312
+ const renderSize = getRenderSize.call(this);
1313
+ const targetRect = rect || {
1314
+ x: 0,
1315
+ y: 0,
1316
+ width: renderSize.width,
1317
+ height: renderSize.height,
1318
+ };
1319
+
1320
+ this[filterRegionsSymbol].push({
1321
+ filter,
1322
+ rect: targetRect,
1323
+ });
1324
+
1325
+ notifyEditorChange.call(this);
1326
+ renderImage.call(this);
1327
+ }
1328
+
1329
+ /**
1330
+ * @private
1331
+ * @param {number} delta
1332
+ */
1333
+ function rotateImage(delta) {
1334
+ if (isReadOnly.call(this)) {
1335
+ return;
1336
+ }
1337
+
1338
+ setRotation.call(this, this[rotationSymbol] + delta);
1339
+ }
1340
+
1341
+ /**
1342
+ * @private
1343
+ * @param {number} rotation
1344
+ */
1345
+ function setRotation(rotation) {
1346
+ if (!this[sourceImageSymbol]) {
1347
+ return;
1348
+ }
1349
+ if (isReadOnly.call(this)) {
1350
+ return;
1351
+ }
1352
+
1353
+ const next = normalizeRotation(rotation);
1354
+ if (next === this[rotationSymbol]) {
1355
+ return;
1356
+ }
1357
+ this[rotationSymbol] = next;
1358
+ this[filterRegionsSymbol] = [];
1359
+ setCropMode.call(this, false);
1360
+ resetViewState.call(this);
1361
+ updateStageAspectFromRenderSize.call(this);
1362
+ updateRotationControl.call(this);
1363
+ notifyEditorChange.call(this);
1364
+ renderImage.call(this);
1365
+ }
1366
+
1367
+ /**
1368
+ * @private
1369
+ * @param {number} rotation
1370
+ * @return {number}
1371
+ */
1372
+ function normalizeRotation(rotation) {
1373
+ let value = Number.isFinite(rotation) ? rotation : 0;
1374
+ value %= 360;
1375
+ if (value < 0) {
1376
+ value += 360;
1377
+ }
1378
+ return value;
1379
+ }
1380
+ /**
1381
+ * @private
1382
+ * @param {CanvasRenderingContext2D} ctx
1383
+ * @param {HTMLImageElement} image
1384
+ */
1385
+ function drawBaseImage(ctx, image, options = {}) {
1386
+ const { imageWidth, imageHeight } = getImageDimensions.call(this);
1387
+ const useViewTransform = options.useViewTransform !== false;
1388
+ ctx.save();
1389
+ if (useViewTransform) {
1390
+ applyViewTransform.call(this, ctx);
1391
+ }
1392
+ applyRotationTransform.call(this, ctx);
1393
+ ctx.drawImage(image, 0, 0, imageWidth, imageHeight);
1394
+ ctx.restore();
1395
+ resetCanvasTransform(ctx);
1396
+ }
1397
+
1398
+ /**
1399
+ * @private
1400
+ * @param {CanvasRenderingContext2D} ctx
1401
+ * @param {HTMLImageElement} image
1402
+ * @param {{filter:string,rect:{x:number,y:number,width:number,height:number}}} region
1403
+ */
1404
+ function drawFilteredRegion(ctx, image, region, options = {}) {
1405
+ if (!region || !region.filter || region.filter === "none") {
1406
+ return;
1407
+ }
1408
+
1409
+ const { imageWidth, imageHeight } = getImageDimensions.call(this);
1410
+ const useViewTransform = options.useViewTransform !== false;
1411
+ ctx.save();
1412
+ if (useViewTransform) {
1413
+ applyViewTransform.call(this, ctx);
1414
+ }
1415
+ applyRotationTransform.call(this, ctx);
1416
+ ctx.filter = region.filter;
1417
+ ctx.beginPath();
1418
+ ctx.rect(region.rect.x, region.rect.y, region.rect.width, region.rect.height);
1419
+ ctx.clip();
1420
+ ctx.drawImage(image, 0, 0, imageWidth, imageHeight);
1421
+ ctx.restore();
1422
+ resetCanvasTransform(ctx);
1423
+ ctx.filter = "none";
1424
+ }
1425
+
1426
+ /**
1427
+ * @private
1428
+ * @param {PointerEvent} event
1429
+ */
1430
+ function startCropSelection(event) {
1431
+ if (this.getOption("features.allowCrop") === false) {
1432
+ return;
1433
+ }
1434
+
1435
+ if (!this[sourceImageSymbol]) {
1436
+ return;
1437
+ }
1438
+
1439
+ if (!this[cropStateSymbol].enabled) {
1440
+ return;
1441
+ }
1442
+
1443
+ const point = getCanvasPoint.call(this, event);
1444
+ const rect = getNormalizedRect.call(this);
1445
+ const handle = rect ? getHandleAtPoint(rect, point) : null;
1446
+
1447
+ this[cropStateSymbol].active = true;
1448
+ this[cropStateSymbol].handle = handle;
1449
+
1450
+ if (handle) {
1451
+ this[cropStateSymbol].mode = "resize";
1452
+ const anchor = getResizeAnchor(rect, handle);
1453
+ this[cropStateSymbol].anchorX = anchor.x;
1454
+ this[cropStateSymbol].anchorY = anchor.y;
1455
+ this[cropStateSymbol].startX = anchor.x;
1456
+ this[cropStateSymbol].startY = anchor.y;
1457
+ this[cropStateSymbol].endX = point.x;
1458
+ this[cropStateSymbol].endY = point.y;
1459
+ } else if (rect && pointInRect(rect, point)) {
1460
+ this[cropStateSymbol].mode = "move";
1461
+ this[cropStateSymbol].offsetX = point.x - rect.x;
1462
+ this[cropStateSymbol].offsetY = point.y - rect.y;
1463
+ } else {
1464
+ this[cropStateSymbol].mode = "draw";
1465
+ this[cropStateSymbol].startX = point.x;
1466
+ this[cropStateSymbol].startY = point.y;
1467
+ this[cropStateSymbol].endX = point.x;
1468
+ this[cropStateSymbol].endY = point.y;
1469
+ }
1470
+
1471
+ this[overlayElementSymbol].setPointerCapture(event.pointerId);
1472
+ drawCropOverlay.call(this);
1473
+ updateCropInputs.call(this);
1474
+ updateCropActionState.call(this);
1475
+ }
1476
+
1477
+ /**
1478
+ * @private
1479
+ * @param {PointerEvent} event
1480
+ */
1481
+ function updateCropSelection(event) {
1482
+ if (!this[cropStateSymbol].active) {
1483
+ return;
1484
+ }
1485
+
1486
+ const point = getCanvasPoint.call(this, event);
1487
+ const mode = this[cropStateSymbol].mode;
1488
+ const imageSize = getImageSize.call(this);
1489
+
1490
+ if (mode === "move") {
1491
+ const rect = getNormalizedRect.call(this);
1492
+ if (!rect || !imageSize) {
1493
+ return;
1494
+ }
1495
+ const newX = clampValue(
1496
+ point.x - this[cropStateSymbol].offsetX,
1497
+ 0,
1498
+ imageSize.width - rect.width,
1499
+ );
1500
+ const newY = clampValue(
1501
+ point.y - this[cropStateSymbol].offsetY,
1502
+ 0,
1503
+ imageSize.height - rect.height,
1504
+ );
1505
+ this[cropStateSymbol].startX = newX;
1506
+ this[cropStateSymbol].startY = newY;
1507
+ this[cropStateSymbol].endX = newX + rect.width;
1508
+ this[cropStateSymbol].endY = newY + rect.height;
1509
+ } else if (mode === "resize") {
1510
+ this[cropStateSymbol].startX = this[cropStateSymbol].anchorX;
1511
+ this[cropStateSymbol].startY = this[cropStateSymbol].anchorY;
1512
+ this[cropStateSymbol].endX = point.x;
1513
+ this[cropStateSymbol].endY = point.y;
1514
+ } else {
1515
+ this[cropStateSymbol].endX = point.x;
1516
+ this[cropStateSymbol].endY = point.y;
1517
+ }
1518
+
1519
+ clampSelectionToImage.call(this);
1520
+ drawCropOverlay.call(this);
1521
+ updateCropInputs.call(this);
1522
+ }
1523
+
1524
+ /**
1525
+ * @private
1526
+ * @param {PointerEvent} event
1527
+ */
1528
+ function finishCropSelection(event) {
1529
+ if (!this[cropStateSymbol].active) {
1530
+ return;
1531
+ }
1532
+
1533
+ if (this[cropStateSymbol].mode !== "move") {
1534
+ const point = getCanvasPoint.call(this, event);
1535
+ this[cropStateSymbol].endX = point.x;
1536
+ this[cropStateSymbol].endY = point.y;
1537
+ }
1538
+ this[cropStateSymbol].active = false;
1539
+ this[cropStateSymbol].mode = "draw";
1540
+ this[cropStateSymbol].handle = null;
1541
+ this[cropStateSymbol].anchorX = 0;
1542
+ this[cropStateSymbol].anchorY = 0;
1543
+ if (this[overlayElementSymbol].hasPointerCapture(event.pointerId)) {
1544
+ this[overlayElementSymbol].releasePointerCapture(event.pointerId);
1545
+ }
1546
+ clampSelectionToImage.call(this);
1547
+ drawCropOverlay.call(this);
1548
+ updateCropInputs.call(this);
1549
+ }
1550
+
1551
+ /**
1552
+ * @private
1553
+ * @return {{x:number,y:number,width:number,height:number}|null}
1554
+ */
1555
+ function getCropRect(options = {}) {
1556
+ const minSize = Number.isFinite(options.minSize) ? options.minSize : 4;
1557
+ const rect = getNormalizedRect.call(this);
1558
+
1559
+ if (!rect || rect.width < minSize || rect.height < minSize) {
1560
+ return null;
1561
+ }
1562
+
1563
+ return rect;
1564
+ }
1565
+
1566
+ /**
1567
+ * @private
1568
+ * @return {{x:number,y:number,width:number,height:number,x2:number,y2:number}|null}
1569
+ */
1570
+ function getNormalizedRect() {
1571
+ const state = this[cropStateSymbol];
1572
+ const width = Math.abs(state.endX - state.startX);
1573
+ const height = Math.abs(state.endY - state.startY);
1574
+
1575
+ if (width === 0 || height === 0) {
1576
+ return null;
1577
+ }
1578
+
1579
+ const x = Math.min(state.startX, state.endX);
1580
+ const y = Math.min(state.startY, state.endY);
1581
+ const x2 = x + width;
1582
+ const y2 = y + height;
1583
+
1584
+ return {
1585
+ x,
1586
+ y,
1587
+ width,
1588
+ height,
1589
+ x2,
1590
+ y2,
1591
+ };
1592
+ }
1593
+
1594
+ /**
1595
+ * @private
1596
+ */
1597
+ function drawCropOverlay() {
1598
+ const overlay = this[overlayElementSymbol];
1599
+ const ctx = overlay.getContext("2d");
1600
+ const rect = getCropRect.call(this, { minSize: 1 });
1601
+
1602
+ resetCanvasTransform(ctx);
1603
+ ctx.clearRect(0, 0, overlay.width, overlay.height);
1604
+
1605
+ if (!this[cropStateSymbol].enabled || !rect) {
1606
+ return;
1607
+ }
1608
+
1609
+ ctx.save();
1610
+ ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
1611
+ ctx.fillRect(0, 0, overlay.width, overlay.height);
1612
+ ctx.globalCompositeOperation = "destination-out";
1613
+ ctx.fillStyle = "rgba(0, 0, 0, 1)";
1614
+ applyViewTransform.call(this, ctx);
1615
+ ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
1616
+ ctx.globalCompositeOperation = "source-over";
1617
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.9)";
1618
+ ctx.lineWidth = 2 / this[viewStateSymbol].scale;
1619
+ ctx.setLineDash([]);
1620
+ ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
1621
+
1622
+ const handleSize = 8 / this[viewStateSymbol].scale;
1623
+ ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
1624
+ for (const [hx, hy] of [
1625
+ [rect.x, rect.y],
1626
+ [rect.x2, rect.y],
1627
+ [rect.x, rect.y2],
1628
+ [rect.x2, rect.y2],
1629
+ ]) {
1630
+ ctx.fillRect(
1631
+ hx - handleSize / 2,
1632
+ hy - handleSize / 2,
1633
+ handleSize,
1634
+ handleSize,
1635
+ );
1636
+ }
1637
+ ctx.restore();
1638
+ resetCanvasTransform(ctx);
1639
+ }
1640
+
1641
+ /**
1642
+ * @private
1643
+ */
1644
+ function handleSelectionButtonClick() {
1645
+ if (isReadOnly.call(this)) {
1646
+ return;
1647
+ }
1648
+
1649
+ const allowFilters = this.getOption("features.allowFilters") !== false;
1650
+ const allowCrop = this.getOption("features.allowCrop") !== false;
1651
+ const allowSelection = allowCrop || allowFilters;
1652
+ if (!allowSelection) {
1653
+ return;
1654
+ }
1655
+
1656
+ if (!this[sourceImageSymbol]) {
1657
+ return;
1658
+ }
1659
+
1660
+ const enabled = !this[cropStateSymbol].enabled;
1661
+ setCropMode.call(this, enabled);
1662
+
1663
+ renderImage.call(this);
1664
+ }
1665
+
1666
+ /**
1667
+ * @private
1668
+ */
1669
+ /**
1670
+ * @private
1671
+ */
1672
+ function handleCropButtonClick() {
1673
+ if (isReadOnly.call(this)) {
1674
+ return;
1675
+ }
1676
+
1677
+ if (this.getOption("features.allowCrop") === false) {
1678
+ return;
1679
+ }
1680
+
1681
+ if (!this[sourceImageSymbol]) {
1682
+ return;
1683
+ }
1684
+
1685
+ const rect = getCropRect.call(this);
1686
+ if (!rect || !this[cropStateSymbol].enabled) {
1687
+ return;
1688
+ }
1689
+
1690
+ applyCrop.call(this);
1691
+ setCropMode.call(this, false);
1692
+ this[filterRegionsSymbol] = [];
1693
+ renderImage.call(this);
1694
+ }
1695
+
1696
+ /**
1697
+ * @private
1698
+ * @param {boolean} enabled
1699
+ */
1700
+ function setCropMode(enabled) {
1701
+ if (enabled && isReadOnly.call(this)) {
1702
+ return;
1703
+ }
1704
+
1705
+ const allowFilters = this.getOption("features.allowFilters") !== false;
1706
+ const allowCrop = this.getOption("features.allowCrop") !== false;
1707
+ const allowSelection = allowCrop || allowFilters;
1708
+ if (!allowSelection && enabled) {
1709
+ return;
1710
+ }
1711
+
1712
+ const effectiveEnabled = allowSelection && enabled;
1713
+ this[cropStateSymbol].enabled = effectiveEnabled;
1714
+ this[cropInputsElementSymbol].hidden = !effectiveEnabled;
1715
+ if (effectiveEnabled) {
1716
+ queueMicrotask(() => {
1717
+ updateCropInputsLayout.call(this);
1718
+ });
1719
+ }
1720
+ this[overlayElementSymbol].style.pointerEvents = effectiveEnabled
1721
+ ? "auto"
1722
+ : "none";
1723
+ this[overlayElementSymbol].style.cursor = effectiveEnabled
1724
+ ? "crosshair"
1725
+ : "default";
1726
+ this[stageElementSymbol].style.cursor = effectiveEnabled
1727
+ ? "crosshair"
1728
+ : this[sourceImageSymbol]
1729
+ ? "grab"
1730
+ : "default";
1731
+ for (const input of [
1732
+ this[cropInputXElementSymbol],
1733
+ this[cropInputYElementSymbol],
1734
+ this[cropInputWidthElementSymbol],
1735
+ this[cropInputHeightElementSymbol],
1736
+ ]) {
1737
+ input.disabled = !effectiveEnabled;
1738
+ }
1739
+
1740
+ if (!effectiveEnabled) {
1741
+ this[cropStateSymbol].active = false;
1742
+ this[cropStateSymbol].handle = null;
1743
+ this[cropStateSymbol].anchorX = 0;
1744
+ this[cropStateSymbol].anchorY = 0;
1745
+ }
1746
+
1747
+ drawCropOverlay.call(this);
1748
+ updateCropInputs.call(this);
1749
+ }
1750
+
1751
+ /**
1752
+ * @private
1753
+ */
1754
+ /**
1755
+ * @private
1756
+ */
1757
+ function applyCropInputs() {
1758
+ if (!this[cropStateSymbol].enabled) {
1759
+ return;
1760
+ }
1761
+ if (isReadOnly.call(this)) {
1762
+ return;
1763
+ }
1764
+
1765
+ const imageSize = getImageSize.call(this);
1766
+ if (!imageSize) {
1767
+ return;
1768
+ }
1769
+
1770
+ const x = Number.parseFloat(this[cropInputXElementSymbol].value || "0");
1771
+ const y = Number.parseFloat(this[cropInputYElementSymbol].value || "0");
1772
+ const width = Number.parseFloat(
1773
+ this[cropInputWidthElementSymbol].value || "0",
1774
+ );
1775
+ const height = Number.parseFloat(
1776
+ this[cropInputHeightElementSymbol].value || "0",
1777
+ );
1778
+
1779
+ const nextX = clampValue(x, 0, imageSize.width);
1780
+ const nextY = clampValue(y, 0, imageSize.height);
1781
+ const nextWidth = clampValue(width, 1, imageSize.width - nextX);
1782
+ const nextHeight = clampValue(height, 1, imageSize.height - nextY);
1783
+
1784
+ this[cropStateSymbol].startX = nextX;
1785
+ this[cropStateSymbol].startY = nextY;
1786
+ this[cropStateSymbol].endX = nextX + nextWidth;
1787
+ this[cropStateSymbol].endY = nextY + nextHeight;
1788
+
1789
+ drawCropOverlay.call(this);
1790
+ updateCropInputs.call(this);
1791
+ }
1792
+
1793
+ /**
1794
+ * @private
1795
+ */
1796
+ function updateCropInputs() {
1797
+ const imageSize = getImageSize.call(this);
1798
+ if (imageSize) {
1799
+ this[cropInputXElementSymbol].max = `${Math.round(imageSize.width)}`;
1800
+ this[cropInputYElementSymbol].max = `${Math.round(imageSize.height)}`;
1801
+ this[cropInputWidthElementSymbol].max = `${Math.round(imageSize.width)}`;
1802
+ this[cropInputHeightElementSymbol].max = `${Math.round(imageSize.height)}`;
1803
+ }
1804
+
1805
+ const rect = getCropRect.call(this, { minSize: 1 });
1806
+
1807
+ if (!rect) {
1808
+ this[cropInputXElementSymbol].value = "0";
1809
+ this[cropInputYElementSymbol].value = "0";
1810
+ this[cropInputWidthElementSymbol].value = "0";
1811
+ this[cropInputHeightElementSymbol].value = "0";
1812
+ updateCropActionState.call(this);
1813
+ return;
1814
+ }
1815
+
1816
+ this[cropInputXElementSymbol].value = `${Math.round(rect.x)}`;
1817
+ this[cropInputYElementSymbol].value = `${Math.round(rect.y)}`;
1818
+ this[cropInputWidthElementSymbol].value = `${Math.round(rect.width)}`;
1819
+ this[cropInputHeightElementSymbol].value = `${Math.round(rect.height)}`;
1820
+ updateCropActionState.call(this);
1821
+ }
1822
+
1823
+ /**
1824
+ * @private
1825
+ */
1826
+ function updateCropActionState() {
1827
+ const hasImage = Boolean(this[sourceImageSymbol]);
1828
+ const allowCrop = this.getOption("features.allowCrop") !== false;
1829
+ const readOnly = isReadOnly.call(this);
1830
+ const rect = getCropRect.call(this);
1831
+ const show =
1832
+ allowCrop &&
1833
+ this[cropStateSymbol].enabled &&
1834
+ Boolean(rect) &&
1835
+ hasImage &&
1836
+ !readOnly;
1837
+
1838
+ this[applyCropButtonElementSymbol].hidden = !show;
1839
+ this[applyCropButtonElementSymbol].style.display = show ? "" : "none";
1840
+ this[applyCropButtonElementSymbol].setOption("disabled", !show);
1841
+ }
1842
+
1843
+ /**
1844
+ * @private
1845
+ */
1846
+ function notifyEditorChange() {
1847
+ fireCustomEvent(this, "monster-image-editor-changed", {
1848
+ element: this,
1849
+ });
1850
+ }
1851
+
1852
+ /**
1853
+ * @private
1854
+ * @return {{width:number,height:number}|null}
1855
+ */
1856
+ function getImageSize() {
1857
+ const image = this[sourceImageSymbol];
1858
+ if (!image) {
1859
+ return null;
1860
+ }
1861
+
1862
+ const { width, height } = getRenderSize.call(this);
1863
+ return { width, height };
1864
+ }
1865
+
1866
+ /**
1867
+ * @private
1868
+ * @param {CanvasRenderingContext2D} ctx
1869
+ */
1870
+ function applyViewTransform(ctx) {
1871
+ const view = this[viewStateSymbol];
1872
+ ctx.setTransform(view.scale, 0, 0, view.scale, view.offsetX, view.offsetY);
1873
+ }
1874
+
1875
+ /**
1876
+ * @private
1877
+ * @param {CanvasRenderingContext2D} ctx
1878
+ */
1879
+ function resetCanvasTransform(ctx) {
1880
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1881
+ }
1882
+
1883
+ /**
1884
+ * @private
1885
+ */
1886
+ function resetViewState() {
1887
+ const view = this[viewStateSymbol];
1888
+ view.scale = 1;
1889
+ view.offsetX = 0;
1890
+ view.offsetY = 0;
1891
+ this[filterRegionsSymbol] = [];
1892
+ updateStageAspectFromRenderSize.call(this);
1893
+ updateZoomControl.call(this);
1894
+ updateRotationControl.call(this);
1895
+ }
1896
+
1897
+ /**
1898
+ * @private
1899
+ * @param {number} scale
1900
+ */
1901
+ function setViewScale(scale) {
1902
+ const view = this[viewStateSymbol];
1903
+ const canvas = this[canvasElementSymbol];
1904
+ const nextScale = clampValue(scale, 0.25, 4);
1905
+
1906
+ if (!canvas) {
1907
+ view.scale = nextScale;
1908
+ updateZoomControl.call(this);
1909
+ return;
1910
+ }
1911
+
1912
+ const centerX = canvas.width / 2;
1913
+ const centerY = canvas.height / 2;
1914
+ const imageCenterX = (centerX - view.offsetX) / view.scale;
1915
+ const imageCenterY = (centerY - view.offsetY) / view.scale;
1916
+
1917
+ view.scale = nextScale;
1918
+ view.offsetX = centerX - imageCenterX * view.scale;
1919
+ view.offsetY = centerY - imageCenterY * view.scale;
1920
+
1921
+ constrainView.call(this);
1922
+ updateZoomControl.call(this);
1923
+ }
1924
+
1925
+ /**
1926
+ * @private
1927
+ * @param {CanvasRenderingContext2D} ctx
1928
+ */
1929
+ function applyRotationTransform(ctx) {
1930
+ const { imageWidth, imageHeight } = getImageDimensions.call(this);
1931
+ const { width, height } = getRenderSize.call(this);
1932
+ const rad = (this[rotationSymbol] * Math.PI) / 180;
1933
+
1934
+ ctx.translate(width / 2, height / 2);
1935
+ ctx.rotate(rad);
1936
+ ctx.translate(-imageWidth / 2, -imageHeight / 2);
1937
+ }
1938
+
1939
+ /**
1940
+ * @private
1941
+ * @return {{width:number,height:number}}
1942
+ */
1943
+ function getRenderSize() {
1944
+ const image = this[sourceImageSymbol];
1945
+ if (!image) {
1946
+ return { width: 0, height: 0 };
1947
+ }
1948
+
1949
+ const imageWidth = image.naturalWidth || image.width;
1950
+ const imageHeight = image.naturalHeight || image.height;
1951
+ const rotation = normalizeRotation(this[rotationSymbol]);
1952
+
1953
+ const rad = (rotation * Math.PI) / 180;
1954
+ const cos = Math.abs(Math.cos(rad));
1955
+ const sin = Math.abs(Math.sin(rad));
1956
+
1957
+ return {
1958
+ width: imageWidth * cos + imageHeight * sin,
1959
+ height: imageWidth * sin + imageHeight * cos,
1960
+ };
1961
+ }
1962
+
1963
+ /**
1964
+ * @private
1965
+ * @return {{imageWidth:number,imageHeight:number}}
1966
+ */
1967
+ function getImageDimensions() {
1968
+ const image = this[sourceImageSymbol];
1969
+ if (!image) {
1970
+ return { imageWidth: 0, imageHeight: 0 };
1971
+ }
1972
+
1973
+ return {
1974
+ imageWidth: image.naturalWidth || image.width,
1975
+ imageHeight: image.naturalHeight || image.height,
1976
+ };
1977
+ }
1978
+
1979
+ /**
1980
+ * @private
1981
+ */
1982
+ function constrainView() {
1983
+ const view = this[viewStateSymbol];
1984
+ const canvas = this[canvasElementSymbol];
1985
+ const imageSize = getRenderSize.call(this);
1986
+
1987
+ if (!canvas || !imageSize) {
1988
+ return;
1989
+ }
1990
+
1991
+ const scaledWidth = imageSize.width * view.scale;
1992
+ const scaledHeight = imageSize.height * view.scale;
1993
+ const canvasWidth = canvas.width;
1994
+ const canvasHeight = canvas.height;
1995
+
1996
+ if (scaledWidth <= canvasWidth) {
1997
+ view.offsetX = (canvasWidth - scaledWidth) / 2;
1998
+ } else {
1999
+ const minX = canvasWidth - scaledWidth;
2000
+ view.offsetX = clampValue(view.offsetX, minX, 0);
2001
+ }
2002
+
2003
+ if (scaledHeight <= canvasHeight) {
2004
+ view.offsetY = (canvasHeight - scaledHeight) / 2;
2005
+ } else {
2006
+ const minY = canvasHeight - scaledHeight;
2007
+ view.offsetY = clampValue(view.offsetY, minY, 0);
2008
+ }
2009
+ }
2010
+
2011
+ /**
2012
+ * @private
2013
+ */
2014
+ function updateZoomControl() {
2015
+ if (!this[zoomInputElementSymbol]) {
2016
+ return;
2017
+ }
2018
+ const view = this[viewStateSymbol];
2019
+ this[zoomInputElementSymbol].value = `${Math.round(view.scale * 100)}`;
2020
+ }
2021
+
2022
+ /**
2023
+ * @private
2024
+ */
2025
+ function updateRotationControl() {
2026
+ if (!this[rotationInputElementSymbol] || !this[rotationRangeElementSymbol]) {
2027
+ return;
2028
+ }
2029
+ const value = `${Math.round(this[rotationSymbol])}`;
2030
+ this[rotationInputElementSymbol].value = value;
2031
+ this[rotationRangeElementSymbol].value = value;
2032
+ }
2033
+
2034
+ /**
2035
+ * @private
2036
+ * @param {PointerEvent} event
2037
+ */
2038
+ function startPan(event) {
2039
+ if (!this[sourceImageSymbol]) {
2040
+ return;
2041
+ }
2042
+
2043
+ if (this[cropStateSymbol].enabled) {
2044
+ return;
2045
+ }
2046
+
2047
+ const view = this[viewStateSymbol];
2048
+ const point = getCanvasPointRaw.call(this, event);
2049
+ view.isPanning = true;
2050
+ view.lastX = point.x;
2051
+ view.lastY = point.y;
2052
+ this[stageElementSymbol].style.cursor = "grabbing";
2053
+ this[stageElementSymbol].setPointerCapture(event.pointerId);
2054
+ }
2055
+
2056
+ /**
2057
+ * @private
2058
+ * @param {PointerEvent} event
2059
+ */
2060
+ function updatePan(event) {
2061
+ const view = this[viewStateSymbol];
2062
+ if (!view.isPanning) {
2063
+ return;
2064
+ }
2065
+
2066
+ const point = getCanvasPointRaw.call(this, event);
2067
+ const deltaX = point.x - view.lastX;
2068
+ const deltaY = point.y - view.lastY;
2069
+ view.lastX = point.x;
2070
+ view.lastY = point.y;
2071
+ view.offsetX += deltaX;
2072
+ view.offsetY += deltaY;
2073
+
2074
+ constrainView.call(this);
2075
+ renderImage.call(this);
2076
+ }
2077
+
2078
+ /**
2079
+ * @private
2080
+ * @param {PointerEvent} event
2081
+ */
2082
+ function stopPan(event) {
2083
+ const view = this[viewStateSymbol];
2084
+ if (!view.isPanning) {
2085
+ return;
2086
+ }
2087
+ view.isPanning = false;
2088
+ if (this[stageElementSymbol].hasPointerCapture(event.pointerId)) {
2089
+ this[stageElementSymbol].releasePointerCapture(event.pointerId);
2090
+ }
2091
+ this[stageElementSymbol].style.cursor = view.moveEnabled
2092
+ ? "grab"
2093
+ : this[cropStateSymbol].enabled
2094
+ ? "crosshair"
2095
+ : this[sourceImageSymbol]
2096
+ ? "grab"
2097
+ : "default";
2098
+ }
2099
+
2100
+ /**
2101
+ * @private
2102
+ */
2103
+ function clampSelectionToImage() {
2104
+ const imageSize = getImageSize.call(this);
2105
+ if (!imageSize) {
2106
+ return;
2107
+ }
2108
+
2109
+ this[cropStateSymbol].startX = clampValue(
2110
+ this[cropStateSymbol].startX,
2111
+ 0,
2112
+ imageSize.width,
2113
+ );
2114
+ this[cropStateSymbol].endX = clampValue(
2115
+ this[cropStateSymbol].endX,
2116
+ 0,
2117
+ imageSize.width,
2118
+ );
2119
+ this[cropStateSymbol].startY = clampValue(
2120
+ this[cropStateSymbol].startY,
2121
+ 0,
2122
+ imageSize.height,
2123
+ );
2124
+ this[cropStateSymbol].endY = clampValue(
2125
+ this[cropStateSymbol].endY,
2126
+ 0,
2127
+ imageSize.height,
2128
+ );
2129
+ }
2130
+
2131
+ /**
2132
+ * @private
2133
+ * @param {number} value
2134
+ * @param {number} min
2135
+ * @param {number} max
2136
+ * @return {number}
2137
+ */
2138
+ function clampValue(value, min, max) {
2139
+ if (!Number.isFinite(value)) {
2140
+ return min;
2141
+ }
2142
+ return Math.min(max, Math.max(min, value));
2143
+ }
2144
+
2145
+ /**
2146
+ * @private
2147
+ * @param {{x:number,y:number,width:number,height:number}} rect
2148
+ * @param {{x:number,y:number}} point
2149
+ * @return {boolean}
2150
+ */
2151
+ function pointInRect(rect, point) {
2152
+ return (
2153
+ point.x >= rect.x &&
2154
+ point.x <= rect.x + rect.width &&
2155
+ point.y >= rect.y &&
2156
+ point.y <= rect.y + rect.height
2157
+ );
2158
+ }
2159
+
2160
+ /**
2161
+ * @private
2162
+ * @param {{x:number,y:number,width:number,height:number,x2:number,y2:number}} rect
2163
+ * @param {{x:number,y:number}} point
2164
+ * @return {string|null}
2165
+ */
2166
+ function getHandleAtPoint(rect, point) {
2167
+ const threshold = 10;
2168
+ const handles = {
2169
+ nw: { x: rect.x, y: rect.y },
2170
+ ne: { x: rect.x2, y: rect.y },
2171
+ sw: { x: rect.x, y: rect.y2 },
2172
+ se: { x: rect.x2, y: rect.y2 },
2173
+ };
2174
+
2175
+ for (const [key, handle] of Object.entries(handles)) {
2176
+ if (
2177
+ Math.abs(point.x - handle.x) <= threshold &&
2178
+ Math.abs(point.y - handle.y) <= threshold
2179
+ ) {
2180
+ return key;
2181
+ }
2182
+ }
2183
+
2184
+ return null;
2185
+ }
2186
+
2187
+ /**
2188
+ * @private
2189
+ * @param {{x:number,y:number,x2:number,y2:number}} rect
2190
+ * @param {string} handle
2191
+ * @return {{x:number,y:number}}
2192
+ */
2193
+ function getResizeAnchor(rect, handle) {
2194
+ switch (handle) {
2195
+ case "nw":
2196
+ return { x: rect.x2, y: rect.y2 };
2197
+ case "ne":
2198
+ return { x: rect.x, y: rect.y2 };
2199
+ case "sw":
2200
+ return { x: rect.x2, y: rect.y };
2201
+ case "se":
2202
+ default:
2203
+ return { x: rect.x, y: rect.y };
2204
+ }
2205
+ }
2206
+
2207
+ /**
2208
+ * @private
2209
+ */
2210
+ function applyCrop() {
2211
+ const image = this[sourceImageSymbol];
2212
+ if (!image) {
2213
+ return;
2214
+ }
2215
+ if (isReadOnly.call(this)) {
2216
+ return;
2217
+ }
2218
+
2219
+ const rect = getCropRect.call(this);
2220
+ if (!rect) {
2221
+ return;
2222
+ }
2223
+
2224
+ const renderSize = getRenderSize.call(this);
2225
+ const baseCanvas = document.createElement("canvas");
2226
+ baseCanvas.width = Math.round(renderSize.width);
2227
+ baseCanvas.height = Math.round(renderSize.height);
2228
+
2229
+ const baseCtx = baseCanvas.getContext("2d");
2230
+ resetCanvasTransform(baseCtx);
2231
+ baseCtx.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
2232
+ drawBaseImage.call(this, baseCtx, image, { useViewTransform: false });
2233
+
2234
+ const cropCanvas = document.createElement("canvas");
2235
+ cropCanvas.width = Math.round(rect.width);
2236
+ cropCanvas.height = Math.round(rect.height);
2237
+
2238
+ const ctx = cropCanvas.getContext("2d");
2239
+ ctx.drawImage(
2240
+ baseCanvas,
2241
+ rect.x,
2242
+ rect.y,
2243
+ rect.width,
2244
+ rect.height,
2245
+ 0,
2246
+ 0,
2247
+ cropCanvas.width,
2248
+ cropCanvas.height,
2249
+ );
2250
+
2251
+ const dataUrl = cropCanvas.toDataURL("image/png");
2252
+ return loadImageFromSource
2253
+ .call(this, dataUrl, {
2254
+ storeOriginal: false,
2255
+ preserveFilters: true,
2256
+ resetView: true,
2257
+ })
2258
+ .then(() => {
2259
+ notifyEditorChange.call(this);
2260
+ });
2261
+ }
2262
+
2263
+ /**
2264
+ * @private
2265
+ * @return {Promise<Blob|null>}
2266
+ */
2267
+ function saveImage() {
2268
+ return getImageBlob.call(this).then((blob) => {
2269
+ if (!blob) {
2270
+ return null;
2271
+ }
2272
+
2273
+ fireCustomEvent(this, "monster-image-editor-saved", {
2274
+ element: this,
2275
+ blob,
2276
+ });
2277
+
2278
+ return blob;
2279
+ });
2280
+ }
2281
+
2282
+ /**
2283
+ * @private
2284
+ * @param {string} type
2285
+ * @param {number} quality
2286
+ * @return {Promise<Blob|null>}
2287
+ */
2288
+ function getImageBlob(type, quality) {
2289
+ const image = this[sourceImageSymbol];
2290
+ if (!image) {
2291
+ return Promise.resolve(null);
2292
+ }
2293
+
2294
+ const outputType = type || this.getOption("output.type");
2295
+ const outputQuality =
2296
+ quality !== undefined ? quality : this.getOption("output.quality");
2297
+ const canvas = renderOutputCanvas.call(this, image);
2298
+
2299
+ return new Promise((resolve) => {
2300
+ canvas.toBlob(
2301
+ (blob) => {
2302
+ resolve(blob || null);
2303
+ },
2304
+ outputType,
2305
+ outputQuality,
2306
+ );
2307
+ });
2308
+ }
2309
+
2310
+ /**
2311
+ * @private
2312
+ * @param {string} type
2313
+ * @param {number} quality
2314
+ * @return {string|null}
2315
+ */
2316
+ function getImageDataUrl(type, quality) {
2317
+ const image = this[sourceImageSymbol];
2318
+ if (!image) {
2319
+ return null;
2320
+ }
2321
+
2322
+ const outputType = type || this.getOption("output.type");
2323
+ const outputQuality =
2324
+ quality !== undefined ? quality : this.getOption("output.quality");
2325
+ const canvas = renderOutputCanvas.call(this, image);
2326
+
2327
+ try {
2328
+ return canvas.toDataURL(outputType, outputQuality);
2329
+ } catch (error) {
2330
+ return null;
2331
+ }
2332
+ }
2333
+
2334
+ /**
2335
+ * @private
2336
+ * @param {HTMLImageElement} image
2337
+ * @return {HTMLCanvasElement}
2338
+ */
2339
+ function renderOutputCanvas(image) {
2340
+ const canvas = document.createElement("canvas");
2341
+ const { width, height } = getRenderSize.call(this);
2342
+ canvas.width = Math.round(width);
2343
+ canvas.height = Math.round(height);
2344
+
2345
+ const ctx = canvas.getContext("2d");
2346
+ resetCanvasTransform(ctx);
2347
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2348
+ drawBaseImage.call(this, ctx, image, { useViewTransform: false });
2349
+
2350
+ const selectionRect = this[cropStateSymbol].enabled
2351
+ ? getCropRect.call(this, { minSize: 1 })
2352
+ : null;
2353
+ const filter = buildFilterString.call(this);
2354
+ const hasFilter = filter !== "none";
2355
+
2356
+ if (this[filterRegionsSymbol].length > 0) {
2357
+ for (const region of this[filterRegionsSymbol]) {
2358
+ drawFilteredRegion.call(this, ctx, image, region, {
2359
+ useViewTransform: false,
2360
+ });
2361
+ }
2362
+ } else if (hasFilter && selectionRect) {
2363
+ drawFilteredRegion.call(
2364
+ this,
2365
+ ctx,
2366
+ image,
2367
+ { filter, rect: selectionRect },
2368
+ { useViewTransform: false },
2369
+ );
2370
+ } else if (hasFilter) {
2371
+ drawFilteredRegion.call(
2372
+ this,
2373
+ ctx,
2374
+ image,
2375
+ { filter, rect: { x: 0, y: 0, width, height } },
2376
+ { useViewTransform: false },
2377
+ );
2378
+ }
2379
+
2380
+ return canvas;
2381
+ }
2382
+
2383
+ /**
2384
+ * @private
2385
+ * @return {Promise<void>|void}
2386
+ */
2387
+ function resetEditor() {
2388
+ resetEditorState.call(this, { keepOriginal: true });
2389
+
2390
+ if (!this[originalSourceSymbol]) {
2391
+ updateUiState.call(this, false);
2392
+ return;
2393
+ }
2394
+
2395
+ return loadImageFromSource.call(this, this[originalSourceSymbol], {
2396
+ storeOriginal: false,
2397
+ });
2398
+ }
2399
+
2400
+ /**
2401
+ * @private
2402
+ * @param {Object} options
2403
+ */
2404
+ function resetEditorState(options = {}) {
2405
+ this[cropStateSymbol] = {
2406
+ enabled: false,
2407
+ active: false,
2408
+ mode: "draw",
2409
+ startX: 0,
2410
+ startY: 0,
2411
+ endX: 0,
2412
+ endY: 0,
2413
+ offsetX: 0,
2414
+ offsetY: 0,
2415
+ handle: null,
2416
+ anchorX: 0,
2417
+ anchorY: 0,
2418
+ };
2419
+
2420
+ this[rotationSymbol] = 0;
2421
+ updateRotationControl.call(this);
2422
+
2423
+ if (!options.preserveFilters) {
2424
+ this[filterSelectElementSymbol].value = "none";
2425
+ this[filterIntensityElementSymbol].value = "100";
2426
+ syncFilterControls.call(this);
2427
+ }
2428
+
2429
+ if (!options.keepOriginal) {
2430
+ this[originalSourceSymbol] = null;
2431
+ }
2432
+ }
2433
+
2434
+ /**
2435
+ * @private
2436
+ * @param {boolean} hasImage
2437
+ */
2438
+ function updateUiState(hasImage) {
2439
+ this[stageElementSymbol].style.display = hasImage ? "block" : "none";
2440
+ this[emptyStateElementSymbol].style.display = hasImage ? "none" : "block";
2441
+
2442
+ const allowCrop = this.getOption("features.allowCrop") !== false;
2443
+ const allowFilters = this.getOption("features.allowFilters") !== false;
2444
+ const allowSelection = allowCrop || allowFilters;
2445
+ const readOnly = isReadOnly.call(this);
2446
+
2447
+ this[applyCropButtonElementSymbol].setOption(
2448
+ "disabled",
2449
+ !hasImage || !allowCrop || readOnly,
2450
+ );
2451
+ this[selectButtonElementSymbol].setOption(
2452
+ "disabled",
2453
+ !hasImage || !allowSelection || readOnly,
2454
+ );
2455
+ this[resetButtonElementSymbol].setOption("disabled", !hasImage || readOnly);
2456
+ this[saveButtonElementSymbol].setOption("disabled", !hasImage || readOnly);
2457
+
2458
+ this[filterSelectElementSymbol].disabled =
2459
+ !hasImage || !allowFilters || readOnly;
2460
+ this[filterIntensityElementSymbol].disabled =
2461
+ !hasImage ||
2462
+ !allowFilters ||
2463
+ readOnly ||
2464
+ this[filterSelectElementSymbol].value === "none";
2465
+ this[filterApplyButtonElementSymbol].setOption(
2466
+ "disabled",
2467
+ !hasImage ||
2468
+ !allowFilters ||
2469
+ readOnly ||
2470
+ this[filterSelectElementSymbol].value === "none",
2471
+ );
2472
+ this[zoomInputElementSymbol].disabled = !hasImage;
2473
+ this[rotationInputElementSymbol].disabled = !hasImage || readOnly;
2474
+ this[rotationRangeElementSymbol].disabled = !hasImage || readOnly;
2475
+ updateZoomControl.call(this);
2476
+
2477
+ const cropInputsDisabled =
2478
+ !hasImage || !allowCrop || !this[cropStateSymbol].enabled || readOnly;
2479
+ for (const input of [
2480
+ this[cropInputXElementSymbol],
2481
+ this[cropInputYElementSymbol],
2482
+ this[cropInputWidthElementSymbol],
2483
+ this[cropInputHeightElementSymbol],
2484
+ ]) {
2485
+ input.disabled = cropInputsDisabled;
2486
+ }
2487
+
2488
+ if (!hasImage && this[cropStateSymbol].enabled) {
2489
+ setCropMode.call(this, false);
2490
+ }
2491
+
2492
+ if (!hasImage) {
2493
+ resetViewState.call(this);
2494
+ }
2495
+
2496
+ if (!this[cropStateSymbol].enabled) {
2497
+ this[stageElementSymbol].style.cursor = hasImage ? "grab" : "default";
2498
+ }
2499
+
2500
+ updateRotationControl.call(this);
2501
+ updateCropActionState.call(this);
2502
+ }
2503
+
2504
+ /**
2505
+ * @private
2506
+ * @param {PointerEvent} event
2507
+ * @return {{x:number,y:number}}
2508
+ */
2509
+ function getCanvasPoint(event) {
2510
+ const raw = getCanvasPointRaw.call(this, event);
2511
+ const view = this[viewStateSymbol];
2512
+ const x = (raw.x - view.offsetX) / view.scale;
2513
+ const y = (raw.y - view.offsetY) / view.scale;
2514
+
2515
+ return {
2516
+ x: Math.max(0, Math.min(this[canvasElementSymbol].width, x)),
2517
+ y: Math.max(0, Math.min(this[canvasElementSymbol].height, y)),
2518
+ };
2519
+ }
2520
+
2521
+ /**
2522
+ * @private
2523
+ * @param {PointerEvent} event
2524
+ * @return {{x:number,y:number}}
2525
+ */
2526
+ function getCanvasPointRaw(event) {
2527
+ const canvas = this[canvasElementSymbol];
2528
+ const rect = canvas.getBoundingClientRect();
2529
+ const scaleX = canvas.width / rect.width;
2530
+ const scaleY = canvas.height / rect.height;
2531
+
2532
+ return {
2533
+ x: (event.clientX - rect.left) * scaleX,
2534
+ y: (event.clientY - rect.top) * scaleY,
2535
+ };
2536
+ }
2537
+
2538
+ /**
2539
+ * @private
2540
+ * @param {string} variable
2541
+ * @return {boolean}
2542
+ */
2543
+ function isURL(variable) {
2544
+ try {
2545
+ new URL(variable);
2546
+ return true;
2547
+ } catch (error) {
2548
+ return false;
2549
+ }
2550
+ }
2551
+
2552
+ /**
2553
+ * @private
2554
+ * @param {Blob} blob
2555
+ * @return {Promise<string>}
2556
+ */
2557
+ function blobToDataUrl(blob) {
2558
+ return new Promise((resolve, reject) => {
2559
+ const reader = new FileReader();
2560
+ reader.onloadend = () => resolve(reader.result);
2561
+ reader.onerror = reject;
2562
+ reader.readAsDataURL(blob);
2563
+ });
2564
+ }
2565
+
2566
+ /**
2567
+ * @private
2568
+ * @returns {{save:string,reset:string,crop:string,select:string,filter:string,intensity:string,zoom:string,rotation:string,empty:string,filterNone:string,filterGrayscale:string,filterSepia:string,filterContrast:string,filterSaturate:string,filterInvert:string,filterBlur:string,cropX:string,cropY:string,cropWidth:string,cropHeight:string,applyFilter:string,rotateLeft:string,rotateRight:string,rotateReset:string}}
2569
+ */
2570
+ function getTranslations() {
2571
+ const language = getLocaleOfDocument().language || "en";
2572
+ const base = language.split("-")[0];
2573
+ const translations = {
2574
+ en: {
2575
+ save: "Save",
2576
+ reset: "Reset",
2577
+ crop: "Crop",
2578
+ select: "Select",
2579
+ filter: "Filter",
2580
+ intensity: "Intensity",
2581
+ zoom: "Zoom",
2582
+ rotation: "Rotation",
2583
+ empty: "No image loaded",
2584
+ filterNone: "No filter",
2585
+ filterGrayscale: "Grayscale",
2586
+ filterSepia: "Sepia",
2587
+ filterContrast: "Contrast",
2588
+ filterSaturate: "Saturate",
2589
+ filterInvert: "Invert",
2590
+ filterBlur: "Blur",
2591
+ cropX: "X",
2592
+ cropY: "Y",
2593
+ cropWidth: "Width",
2594
+ cropHeight: "Height",
2595
+ applyFilter: "Apply Filter",
2596
+ rotateLeft: "Rotate Left",
2597
+ rotateRight: "Rotate Right",
2598
+ rotateReset: "Reset Rotation",
2599
+ },
2600
+ de: {
2601
+ save: "Speichern",
2602
+ reset: "Zurücksetzen",
2603
+ crop: "Zuschneiden",
2604
+ select: "Auswahl",
2605
+ filter: "Filter",
2606
+ intensity: "Intensität",
2607
+ zoom: "Zoom",
2608
+ rotation: "Drehung",
2609
+ empty: "Kein Bild geladen",
2610
+ filterNone: "Kein Filter",
2611
+ filterGrayscale: "Graustufen",
2612
+ filterSepia: "Sepia",
2613
+ filterContrast: "Kontrast",
2614
+ filterSaturate: "Sättigung",
2615
+ filterInvert: "Invertieren",
2616
+ filterBlur: "Weichzeichnen",
2617
+ cropX: "X",
2618
+ cropY: "Y",
2619
+ cropWidth: "Breite",
2620
+ cropHeight: "Höhe",
2621
+ applyFilter: "Filter anwenden",
2622
+ rotateLeft: "Links drehen",
2623
+ rotateRight: "Rechts drehen",
2624
+ rotateReset: "Drehung zurücksetzen",
2625
+ },
2626
+ es: {
2627
+ save: "Guardar",
2628
+ reset: "Restablecer",
2629
+ crop: "Recortar",
2630
+ select: "Selección",
2631
+ filter: "Filtro",
2632
+ intensity: "Intensidad",
2633
+ zoom: "Zoom",
2634
+ rotation: "Rotación",
2635
+ empty: "No hay imagen cargada",
2636
+ filterNone: "Sin filtro",
2637
+ filterGrayscale: "Escala de grises",
2638
+ filterSepia: "Sepia",
2639
+ filterContrast: "Contraste",
2640
+ filterSaturate: "Saturación",
2641
+ filterInvert: "Invertir",
2642
+ filterBlur: "Desenfocar",
2643
+ cropX: "X",
2644
+ cropY: "Y",
2645
+ cropWidth: "Ancho",
2646
+ cropHeight: "Alto",
2647
+ applyFilter: "Aplicar filtro",
2648
+ rotateLeft: "Girar a la izquierda",
2649
+ rotateRight: "Girar a la derecha",
2650
+ rotateReset: "Restablecer rotación",
2651
+ },
2652
+ zh: {
2653
+ save: "保存",
2654
+ reset: "重置",
2655
+ crop: "裁剪",
2656
+ select: "选择",
2657
+ filter: "滤镜",
2658
+ intensity: "强度",
2659
+ zoom: "缩放",
2660
+ rotation: "旋转",
2661
+ empty: "未加载图片",
2662
+ filterNone: "无滤镜",
2663
+ filterGrayscale: "灰度",
2664
+ filterSepia: "棕褐色",
2665
+ filterContrast: "对比度",
2666
+ filterSaturate: "饱和度",
2667
+ filterInvert: "反相",
2668
+ filterBlur: "模糊",
2669
+ cropX: "X",
2670
+ cropY: "Y",
2671
+ cropWidth: "宽度",
2672
+ cropHeight: "高度",
2673
+ applyFilter: "应用滤镜",
2674
+ rotateLeft: "向左旋转",
2675
+ rotateRight: "向右旋转",
2676
+ rotateReset: "重置旋转",
2677
+ },
2678
+ hi: {
2679
+ save: "सहेजें",
2680
+ reset: "रीसेट",
2681
+ crop: "क्रॉप",
2682
+ select: "चयन",
2683
+ filter: "फ़िल्टर",
2684
+ intensity: "तीव्रता",
2685
+ zoom: "ज़ूम",
2686
+ rotation: "घुमाव",
2687
+ empty: "कोई छवि लोड नहीं",
2688
+ filterNone: "कोई फ़िल्टर नहीं",
2689
+ filterGrayscale: "ग्रे-स्केल",
2690
+ filterSepia: "सेपिया",
2691
+ filterContrast: "कॉन्ट्रास्ट",
2692
+ filterSaturate: "संतृप्ति",
2693
+ filterInvert: "इनवर्ट",
2694
+ filterBlur: "धुंधला",
2695
+ cropX: "X",
2696
+ cropY: "Y",
2697
+ cropWidth: "चौड़ाई",
2698
+ cropHeight: "ऊँचाई",
2699
+ applyFilter: "फ़िल्टर लागू करें",
2700
+ rotateLeft: "बाएँ घुमाएँ",
2701
+ rotateRight: "दाएँ घुमाएँ",
2702
+ rotateReset: "घुमाव रीसेट",
2703
+ },
2704
+ bn: {
2705
+ save: "সংরক্ষণ",
2706
+ reset: "রিসেট",
2707
+ crop: "ক্রপ",
2708
+ select: "নির্বাচন",
2709
+ filter: "ফিল্টার",
2710
+ intensity: "তীব্রতা",
2711
+ zoom: "জুম",
2712
+ rotation: "ঘূর্ণন",
2713
+ empty: "কোনো ছবি লোড নেই",
2714
+ filterNone: "কোনো ফিল্টার নেই",
2715
+ filterGrayscale: "ধূসর",
2716
+ filterSepia: "সেপিয়া",
2717
+ filterContrast: "কনট্রাস্ট",
2718
+ filterSaturate: "স্যাচুরেশন",
2719
+ filterInvert: "ইনভার্ট",
2720
+ filterBlur: "ব্লার",
2721
+ cropX: "X",
2722
+ cropY: "Y",
2723
+ cropWidth: "প্রস্থ",
2724
+ cropHeight: "উচ্চতা",
2725
+ applyFilter: "ফিল্টার প্রয়োগ",
2726
+ rotateLeft: "বামে ঘোরান",
2727
+ rotateRight: "ডানে ঘোরান",
2728
+ rotateReset: "ঘূর্ণন রিসেট",
2729
+ },
2730
+ pt: {
2731
+ save: "Salvar",
2732
+ reset: "Repor",
2733
+ crop: "Cortar",
2734
+ select: "Selecionar",
2735
+ filter: "Filtro",
2736
+ intensity: "Intensidade",
2737
+ zoom: "Zoom",
2738
+ rotation: "Rotação",
2739
+ empty: "Nenhuma imagem carregada",
2740
+ filterNone: "Sem filtro",
2741
+ filterGrayscale: "Escala de cinzentos",
2742
+ filterSepia: "Sépia",
2743
+ filterContrast: "Contraste",
2744
+ filterSaturate: "Saturação",
2745
+ filterInvert: "Inverter",
2746
+ filterBlur: "Desfocar",
2747
+ cropX: "X",
2748
+ cropY: "Y",
2749
+ cropWidth: "Largura",
2750
+ cropHeight: "Altura",
2751
+ applyFilter: "Aplicar filtro",
2752
+ rotateLeft: "Rodar à esquerda",
2753
+ rotateRight: "Rodar à direita",
2754
+ rotateReset: "Repor rotação",
2755
+ },
2756
+ ru: {
2757
+ save: "Сохранить",
2758
+ reset: "Сбросить",
2759
+ crop: "Обрезать",
2760
+ select: "Выбор",
2761
+ filter: "Фильтр",
2762
+ intensity: "Интенсивность",
2763
+ zoom: "Масштаб",
2764
+ rotation: "Поворот",
2765
+ empty: "Изображение не загружено",
2766
+ filterNone: "Без фильтра",
2767
+ filterGrayscale: "Оттенки серого",
2768
+ filterSepia: "Сепия",
2769
+ filterContrast: "Контраст",
2770
+ filterSaturate: "Насыщенность",
2771
+ filterInvert: "Инвертировать",
2772
+ filterBlur: "Размытие",
2773
+ cropX: "X",
2774
+ cropY: "Y",
2775
+ cropWidth: "Ширина",
2776
+ cropHeight: "Высота",
2777
+ applyFilter: "Применить фильтр",
2778
+ rotateLeft: "Повернуть влево",
2779
+ rotateRight: "Повернуть вправо",
2780
+ rotateReset: "Сбросить поворот",
2781
+ },
2782
+ ja: {
2783
+ save: "保存",
2784
+ reset: "リセット",
2785
+ crop: "切り抜き",
2786
+ select: "選択",
2787
+ filter: "フィルター",
2788
+ intensity: "強度",
2789
+ zoom: "ズーム",
2790
+ rotation: "回転",
2791
+ empty: "画像が読み込まれていません",
2792
+ filterNone: "フィルターなし",
2793
+ filterGrayscale: "グレースケール",
2794
+ filterSepia: "セピア",
2795
+ filterContrast: "コントラスト",
2796
+ filterSaturate: "彩度",
2797
+ filterInvert: "反転",
2798
+ filterBlur: "ぼかし",
2799
+ cropX: "X",
2800
+ cropY: "Y",
2801
+ cropWidth: "幅",
2802
+ cropHeight: "高さ",
2803
+ applyFilter: "フィルターを適用",
2804
+ rotateLeft: "左に回転",
2805
+ rotateRight: "右に回転",
2806
+ rotateReset: "回転をリセット",
2807
+ },
2808
+ pa: {
2809
+ save: "ਸੇਵ ਕਰੋ",
2810
+ reset: "ਰੀਸੈਟ",
2811
+ crop: "ਕ੍ਰੌਪ",
2812
+ select: "ਚੋਣ",
2813
+ filter: "ਫਿਲਟਰ",
2814
+ intensity: "ਤੀਬਰਤਾ",
2815
+ zoom: "ਜ਼ੂਮ",
2816
+ rotation: "ਘੁੰਮਾਉ",
2817
+ empty: "ਕੋਈ ਚਿੱਤਰ ਲੋਡ ਨਹੀਂ",
2818
+ filterNone: "ਕੋਈ ਫਿਲਟਰ ਨਹੀਂ",
2819
+ filterGrayscale: "ਸਲੇਟੀ ਪੱਧਰ",
2820
+ filterSepia: "ਸੈਪੀਆ",
2821
+ filterContrast: "ਕਾਂਟਰਾਸਟ",
2822
+ filterSaturate: "ਸੈਚੂਰੇਸ਼ਨ",
2823
+ filterInvert: "ਉਲਟੋ",
2824
+ filterBlur: "ਧੁੰਦਲਾ",
2825
+ cropX: "X",
2826
+ cropY: "Y",
2827
+ cropWidth: "ਚੌੜਾਈ",
2828
+ cropHeight: "ਉਚਾਈ",
2829
+ applyFilter: "ਫਿਲਟਰ ਲਾਗੂ ਕਰੋ",
2830
+ rotateLeft: "ਖੱਬੇ ਘੁੰਮਾਓ",
2831
+ rotateRight: "ਸੱਜੇ ਘੁੰਮਾਓ",
2832
+ rotateReset: "ਘੁੰਮਾਉ ਰੀਸੈਟ",
2833
+ },
2834
+ mr: {
2835
+ save: "जतन करा",
2836
+ reset: "रीसेट",
2837
+ crop: "क्रॉप",
2838
+ select: "निवडा",
2839
+ filter: "फिल्टर",
2840
+ intensity: "तीव्रता",
2841
+ zoom: "झूम",
2842
+ rotation: "फिरवणे",
2843
+ empty: "प्रतिमा लोड नाही",
2844
+ filterNone: "कोणताही फिल्टर नाही",
2845
+ filterGrayscale: "करड्या छटा",
2846
+ filterSepia: "सेपिया",
2847
+ filterContrast: "काँट्रास्ट",
2848
+ filterSaturate: "सॅच्युरेशन",
2849
+ filterInvert: "उलट",
2850
+ filterBlur: "धूसर",
2851
+ cropX: "X",
2852
+ cropY: "Y",
2853
+ cropWidth: "रुंदी",
2854
+ cropHeight: "उंची",
2855
+ applyFilter: "फिल्टर लागू करा",
2856
+ rotateLeft: "डावीकडे फिरवा",
2857
+ rotateRight: "उजवीकडे फिरवा",
2858
+ rotateReset: "फिरवणे रीसेट",
2859
+ },
2860
+ fr: {
2861
+ save: "Enregistrer",
2862
+ reset: "Réinitialiser",
2863
+ crop: "Rogner",
2864
+ select: "Sélection",
2865
+ filter: "Filtre",
2866
+ intensity: "Intensité",
2867
+ zoom: "Zoom",
2868
+ rotation: "Rotation",
2869
+ empty: "Aucune image chargée",
2870
+ filterNone: "Sans filtre",
2871
+ filterGrayscale: "Niveaux de gris",
2872
+ filterSepia: "Sépia",
2873
+ filterContrast: "Contraste",
2874
+ filterSaturate: "Saturation",
2875
+ filterInvert: "Inverser",
2876
+ filterBlur: "Flou",
2877
+ cropX: "X",
2878
+ cropY: "Y",
2879
+ cropWidth: "Largeur",
2880
+ cropHeight: "Hauteur",
2881
+ applyFilter: "Appliquer le filtre",
2882
+ rotateLeft: "Tourner à gauche",
2883
+ rotateRight: "Tourner à droite",
2884
+ rotateReset: "Réinitialiser la rotation",
2885
+ },
2886
+ it: {
2887
+ save: "Salva",
2888
+ reset: "Reimposta",
2889
+ crop: "Ritaglia",
2890
+ select: "Selezione",
2891
+ filter: "Filtro",
2892
+ intensity: "Intensità",
2893
+ zoom: "Zoom",
2894
+ rotation: "Rotazione",
2895
+ empty: "Nessuna immagine caricata",
2896
+ filterNone: "Nessun filtro",
2897
+ filterGrayscale: "Scala di grigi",
2898
+ filterSepia: "Seppia",
2899
+ filterContrast: "Contrasto",
2900
+ filterSaturate: "Saturazione",
2901
+ filterInvert: "Inverti",
2902
+ filterBlur: "Sfocatura",
2903
+ cropX: "X",
2904
+ cropY: "Y",
2905
+ cropWidth: "Larghezza",
2906
+ cropHeight: "Altezza",
2907
+ applyFilter: "Applica filtro",
2908
+ rotateLeft: "Ruota a sinistra",
2909
+ rotateRight: "Ruota a destra",
2910
+ rotateReset: "Reimposta rotazione",
2911
+ },
2912
+ nl: {
2913
+ save: "Opslaan",
2914
+ reset: "Resetten",
2915
+ crop: "Bijsnijden",
2916
+ select: "Selectie",
2917
+ filter: "Filter",
2918
+ intensity: "Intensiteit",
2919
+ zoom: "Zoom",
2920
+ rotation: "Rotatie",
2921
+ empty: "Geen afbeelding geladen",
2922
+ filterNone: "Geen filter",
2923
+ filterGrayscale: "Grijswaarden",
2924
+ filterSepia: "Sepia",
2925
+ filterContrast: "Contrast",
2926
+ filterSaturate: "Verzadiging",
2927
+ filterInvert: "Inverteren",
2928
+ filterBlur: "Vervagen",
2929
+ cropX: "X",
2930
+ cropY: "Y",
2931
+ cropWidth: "Breedte",
2932
+ cropHeight: "Hoogte",
2933
+ applyFilter: "Filter toepassen",
2934
+ rotateLeft: "Draai links",
2935
+ rotateRight: "Draai rechts",
2936
+ rotateReset: "Rotatie resetten",
2937
+ },
2938
+ sv: {
2939
+ save: "Spara",
2940
+ reset: "Återställ",
2941
+ crop: "Beskär",
2942
+ select: "Val",
2943
+ filter: "Filter",
2944
+ intensity: "Intensitet",
2945
+ zoom: "Zoom",
2946
+ rotation: "Rotation",
2947
+ empty: "Ingen bild laddad",
2948
+ filterNone: "Inget filter",
2949
+ filterGrayscale: "Gråskala",
2950
+ filterSepia: "Sepia",
2951
+ filterContrast: "Kontrast",
2952
+ filterSaturate: "Mättnad",
2953
+ filterInvert: "Invertera",
2954
+ filterBlur: "Oskärpa",
2955
+ cropX: "X",
2956
+ cropY: "Y",
2957
+ cropWidth: "Bredd",
2958
+ cropHeight: "Höjd",
2959
+ applyFilter: "Applicera filter",
2960
+ rotateLeft: "Rotera vänster",
2961
+ rotateRight: "Rotera höger",
2962
+ rotateReset: "Återställ rotation",
2963
+ },
2964
+ pl: {
2965
+ save: "Zapisz",
2966
+ reset: "Resetuj",
2967
+ crop: "Przytnij",
2968
+ select: "Zaznacz",
2969
+ filter: "Filtr",
2970
+ intensity: "Intensywność",
2971
+ zoom: "Zoom",
2972
+ rotation: "Obrót",
2973
+ empty: "Brak załadowanego obrazu",
2974
+ filterNone: "Brak filtra",
2975
+ filterGrayscale: "Skala szarości",
2976
+ filterSepia: "Sepia",
2977
+ filterContrast: "Kontrast",
2978
+ filterSaturate: "Nasycenie",
2979
+ filterInvert: "Odwróć",
2980
+ filterBlur: "Rozmycie",
2981
+ cropX: "X",
2982
+ cropY: "Y",
2983
+ cropWidth: "Szerokość",
2984
+ cropHeight: "Wysokość",
2985
+ applyFilter: "Zastosuj filtr",
2986
+ rotateLeft: "Obróć w lewo",
2987
+ rotateRight: "Obróć w prawo",
2988
+ rotateReset: "Resetuj obrót",
2989
+ },
2990
+ da: {
2991
+ save: "Gem",
2992
+ reset: "Nulstil",
2993
+ crop: "Beskær",
2994
+ select: "Vælg",
2995
+ filter: "Filter",
2996
+ intensity: "Intensitet",
2997
+ zoom: "Zoom",
2998
+ rotation: "Rotation",
2999
+ empty: "Intet billede indlæst",
3000
+ filterNone: "Ingen filter",
3001
+ filterGrayscale: "Gråtoner",
3002
+ filterSepia: "Sepia",
3003
+ filterContrast: "Kontrast",
3004
+ filterSaturate: "Mætning",
3005
+ filterInvert: "Inverter",
3006
+ filterBlur: "Sløring",
3007
+ cropX: "X",
3008
+ cropY: "Y",
3009
+ cropWidth: "Bredde",
3010
+ cropHeight: "Højde",
3011
+ applyFilter: "Anvend filter",
3012
+ rotateLeft: "Drej venstre",
3013
+ rotateRight: "Drej højre",
3014
+ rotateReset: "Nulstil rotation",
3015
+ },
3016
+ fi: {
3017
+ save: "Tallenna",
3018
+ reset: "Palauta",
3019
+ crop: "Rajaa",
3020
+ select: "Valitse",
3021
+ filter: "Suodin",
3022
+ intensity: "Voimakkuus",
3023
+ zoom: "Zoom",
3024
+ rotation: "Kierto",
3025
+ empty: "Kuvaa ei ladattu",
3026
+ filterNone: "Ei suodatinta",
3027
+ filterGrayscale: "Harmaasävy",
3028
+ filterSepia: "Sepia",
3029
+ filterContrast: "Kontrasti",
3030
+ filterSaturate: "Kylläisyys",
3031
+ filterInvert: "Käännä",
3032
+ filterBlur: "Sumennus",
3033
+ cropX: "X",
3034
+ cropY: "Y",
3035
+ cropWidth: "Leveys",
3036
+ cropHeight: "Korkeus",
3037
+ applyFilter: "Käytä suodinta",
3038
+ rotateLeft: "Kierrä vasemmalle",
3039
+ rotateRight: "Kierrä oikealle",
3040
+ rotateReset: "Palauta kierto",
3041
+ },
3042
+ no: {
3043
+ save: "Lagre",
3044
+ reset: "Tilbakestill",
3045
+ crop: "Beskjær",
3046
+ select: "Velg",
3047
+ filter: "Filter",
3048
+ intensity: "Intensitet",
3049
+ zoom: "Zoom",
3050
+ rotation: "Rotasjon",
3051
+ empty: "Ingen bilde lastet",
3052
+ filterNone: "Ingen filter",
3053
+ filterGrayscale: "Gråskala",
3054
+ filterSepia: "Sepia",
3055
+ filterContrast: "Kontrast",
3056
+ filterSaturate: "Metning",
3057
+ filterInvert: "Inverter",
3058
+ filterBlur: "Uskarp",
3059
+ cropX: "X",
3060
+ cropY: "Y",
3061
+ cropWidth: "Bredde",
3062
+ cropHeight: "Høyde",
3063
+ applyFilter: "Bruk filter",
3064
+ rotateLeft: "Roter til venstre",
3065
+ rotateRight: "Roter til høyre",
3066
+ rotateReset: "Tilbakestill rotasjon",
3067
+ },
3068
+ cs: {
3069
+ save: "Uložit",
3070
+ reset: "Resetovat",
3071
+ crop: "Oříznout",
3072
+ select: "Výběr",
3073
+ filter: "Filtr",
3074
+ intensity: "Intenzita",
3075
+ zoom: "Zoom",
3076
+ rotation: "Rotace",
3077
+ empty: "Žádný obrázek není načten",
3078
+ filterNone: "Bez filtru",
3079
+ filterGrayscale: "Stupně šedi",
3080
+ filterSepia: "Sepia",
3081
+ filterContrast: "Kontrast",
3082
+ filterSaturate: "Sytost",
3083
+ filterInvert: "Invertovat",
3084
+ filterBlur: "Rozmazání",
3085
+ cropX: "X",
3086
+ cropY: "Y",
3087
+ cropWidth: "Šířka",
3088
+ cropHeight: "Výška",
3089
+ applyFilter: "Použít filtr",
3090
+ rotateLeft: "Otočit doleva",
3091
+ rotateRight: "Otočit doprava",
3092
+ rotateReset: "Resetovat rotaci",
3093
+ },
3094
+ };
3095
+
3096
+ return translations[base] || translations.en;
3097
+ }
3098
+
3099
+ /**
3100
+ * @private
3101
+ * @return {string}
3102
+ */
3103
+ function getTemplate() {
3104
+ // language=HTML
3105
+ return `
3106
+ <style>
3107
+ :host {
3108
+ box-sizing: border-box;
3109
+ display: block;
3110
+ height: 100%;
3111
+ min-height: 320px;
3112
+ }
3113
+ [data-monster-role="control"] {
3114
+ box-sizing: border-box;
3115
+ display: flex;
3116
+ flex-direction: column;
3117
+ height: 100%;
3118
+ min-height: 0;
3119
+ }
3120
+ [data-monster-role="splitPanel"] {
3121
+ flex: 1;
3122
+ min-height: 0;
3123
+ }
3124
+ [data-monster-role="startContent"],
3125
+ [data-monster-role="endContent"] {
3126
+ min-height: 0;
3127
+ }
3128
+ [data-monster-role="endContent"] {
3129
+ display: flex;
3130
+ flex-direction: column;
3131
+ }
3132
+ [data-monster-role="stage"] {
3133
+ flex: 1;
3134
+ min-height: 0;
3135
+ }
3136
+ [data-monster-role="startContent"]::-webkit-scrollbar,
3137
+ [data-monster-role="endContent"]::-webkit-scrollbar {
3138
+ width: 6px;
3139
+ height: 6px;
3140
+ }
3141
+ [data-monster-role="startContent"]::-webkit-scrollbar-thumb,
3142
+ [data-monster-role="endContent"]::-webkit-scrollbar-thumb {
3143
+ background: var(--monster-color-gray-4);
3144
+ }
3145
+ </style>
3146
+ <div data-monster-role="control" part="control">
3147
+ <monster-state data-monster-role="emptyState">
3148
+ <p class="empty-text"
3149
+ data-monster-replace="path:labels.empty | default:No image loaded:string"></p>
3150
+ </monster-state>
3151
+
3152
+ <monster-split-panel data-monster-role="splitPanel"
3153
+ data-monster-options='{"splitType":"vertical","dimension":{"initial":"40%","min":"20%","max":"60%"}}'>
3154
+ <div slot="start" data-monster-role="startContent" class="monster-padding-3">
3155
+ <monster-button-bar class="monster-margin-bottom-3" data-monster-role="topActions">
3156
+ <monster-message-state-button part="resetButton" data-monster-role="reset"
3157
+ data-monster-replace="path:labels.reset"></monster-message-state-button>
3158
+ <monster-message-state-button part="saveButton" data-monster-role="save"
3159
+ data-monster-replace="path:labels.save"></monster-message-state-button>
3160
+ <slot name="actions"></slot>
3161
+ </monster-button-bar>
3162
+
3163
+ <monster-field-set class="monster-margin-bottom-3" data-monster-role="viewSection"
3164
+ data-monster-options='{"labels":{"title":"View"},"features":{"multipleColumns":false}}'>
3165
+ <label>
3166
+ <span data-monster-replace="path:labels.zoom"></span>
3167
+ <input type="range" min="25" max="400" value="100" data-monster-role="zoomInput" />
3168
+ </label>
3169
+ <label>
3170
+ <span data-monster-replace="path:labels.rotation"></span>
3171
+ <input type="range" min="0" max="359" step="1" value="0" data-monster-role="rotationRange" />
3172
+ </label>
3173
+ <label class="monster-margin-top-2">
3174
+ <span data-monster-replace="path:labels.rotation"></span>
3175
+ <input type="number" min="0" max="359" step="1" value="0" data-monster-role="rotationInput" />
3176
+ </label>
3177
+ <monster-button-bar class="monster-margin-top-3">
3178
+ <monster-message-state-button part="rotateLeftButton" data-monster-role="rotateLeft"
3179
+ data-monster-replace="path:labels.rotateLeft"></monster-message-state-button>
3180
+ <monster-message-state-button part="rotateRightButton" data-monster-role="rotateRight"
3181
+ data-monster-replace="path:labels.rotateRight"></monster-message-state-button>
3182
+ <monster-message-state-button part="rotateResetButton" data-monster-role="rotateReset"
3183
+ data-monster-replace="path:labels.rotateReset"></monster-message-state-button>
3184
+ </monster-button-bar>
3185
+ </monster-field-set>
3186
+
3187
+ <monster-field-set class="monster-margin-bottom-3" data-monster-role="selectionSection"
3188
+ data-monster-options='{"labels":{"title":"Selection"},"features":{"multipleColumns":false}}'>
3189
+ <monster-button-bar class="monster-margin-bottom-2">
3190
+ <monster-message-state-button part="selectButton" data-monster-role="select"
3191
+ data-monster-replace="path:labels.select"></monster-message-state-button>
3192
+ <monster-message-state-button part="cropButton" data-monster-role="applyCrop"
3193
+ data-monster-replace="path:labels.crop"></monster-message-state-button>
3194
+ </monster-button-bar>
3195
+ <div class="monster-margin-top-2" data-monster-role="cropInputs">
3196
+ <label>
3197
+ <span data-monster-replace="path:labels.cropX"></span>
3198
+ <input type="number" min="0" step="1" data-monster-role="cropInputX" />
3199
+ </label>
3200
+ <label>
3201
+ <span data-monster-replace="path:labels.cropY"></span>
3202
+ <input type="number" min="0" step="1" data-monster-role="cropInputY" />
3203
+ </label>
3204
+ <label>
3205
+ <span data-monster-replace="path:labels.cropWidth"></span>
3206
+ <input type="number" min="1" step="1" data-monster-role="cropInputWidth" />
3207
+ </label>
3208
+ <label>
3209
+ <span data-monster-replace="path:labels.cropHeight"></span>
3210
+ <input type="number" min="1" step="1" data-monster-role="cropInputHeight" />
3211
+ </label>
3212
+ </div>
3213
+ </monster-field-set>
3214
+
3215
+ <monster-field-set class="monster-margin-bottom-3" data-monster-role="filterSection"
3216
+ data-monster-options='{"labels":{"title":"Filter"},"features":{"multipleColumns":false}}'>
3217
+ <label>
3218
+ <span data-monster-replace="path:labels.filter"></span>
3219
+ <select data-monster-role="filterSelect">
3220
+ <option value="none" data-monster-replace="path:labels.filterNone"></option>
3221
+ <option value="grayscale" data-monster-replace="path:labels.filterGrayscale"></option>
3222
+ <option value="sepia" data-monster-replace="path:labels.filterSepia"></option>
3223
+ <option value="contrast" data-monster-replace="path:labels.filterContrast"></option>
3224
+ <option value="saturate" data-monster-replace="path:labels.filterSaturate"></option>
3225
+ <option value="invert" data-monster-replace="path:labels.filterInvert"></option>
3226
+ <option value="blur" data-monster-replace="path:labels.filterBlur"></option>
3227
+ </select>
3228
+ </label>
3229
+ <label>
3230
+ <span data-monster-replace="path:labels.intensity"></span>
3231
+ <input type="range" min="0" max="100" value="100" data-monster-role="filterIntensity" />
3232
+ </label>
3233
+ <monster-button-bar class="monster-margin-top-2">
3234
+ <monster-message-state-button part="applyFilterButton" data-monster-role="applyFilter"
3235
+ data-monster-replace="path:labels.applyFilter"></monster-message-state-button>
3236
+ </monster-button-bar>
3237
+ </monster-field-set>
3238
+ </div>
3239
+ <div slot="end" data-monster-role="endContent" class="monster-padding-3">
3240
+ <div data-monster-role="stage">
3241
+ <div data-monster-role="surface">
3242
+ <canvas data-monster-role="canvas"></canvas>
3243
+ <canvas data-monster-role="overlay"></canvas>
3244
+ </div>
3245
+ </div>
3246
+ </div>
3247
+ </monster-split-panel>
3248
+ </div>`;
3249
+ }
3250
+
3251
+ registerCustomElement(ImageEditor);