js-superellipse 1.3.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,4228 @@
1
+ /**
2
+ *
3
+ * @module sj-superellipse
4
+ * @version 1.0.0
5
+ * @author f4n70m
6
+ * @license MIT
7
+ *
8
+ * @description
9
+ * Библиотека для применения суперэллипсов к произвольным DOM-элементам.
10
+ * Позволяет плавно изменять форму углов от вогнутых прямоугольных (-2) до выгнутых прямоугольных (2),
11
+ * проходя через скос (0), круглые углы (±G) и классический суперэллипс (1).
12
+ *
13
+ * Особенности:
14
+ * - Два режима работы: `clip-path` (легковесный) и `svg-layer` (полнофункциональный с поддержкой границ и теней).
15
+ * - Автоматическое отслеживание изменений размеров, стилей, видимости и атрибутов элемента.
16
+ * - Поддержка `border-radius`, `border`, `box-shadow`, `background` в режиме `svg-layer`.
17
+ * - Несколько способов инициализации: через селектор, элемент, коллекцию или глобальную функцию.
18
+ * - Умное кэширование стилей для минимизации перерисовок.
19
+ *
20
+ * @typicalname superellipse
21
+ *
22
+ * @example
23
+ * // Инициализация элемента с режимом svg-layer (по умолчанию)
24
+ * const element = document.querySelector('.my-element');
25
+ * const controller = element.superellipseInit({
26
+ * curveFactor: 1.2,
27
+ * precision: 3
28
+ * });
29
+ *
30
+ * // Изменение коэффициента кривизны
31
+ * controller.setCurveFactor(0.8);
32
+ *
33
+ * // Переключение режима
34
+ * controller.switchMode('clip-path');
35
+ *
36
+ * // Инициализация всех элементов с классом .rounded
37
+ * superellipseInit('.rounded', { mode: 'clip-path' });
38
+ *
39
+ * @example
40
+ * // Генерация только SVG-пути без привязки к DOM
41
+ * import { jsse_generateSuperellipsePath } from 'js-superellipse';
42
+ * const path = jsse_generateSuperellipsePath(200, 150, 20, 1.5);
43
+ * document.querySelector('path').setAttribute('d', path);
44
+ */
45
+
46
+ /**
47
+ * Глобальная функция для инициализации суперэллипса на одном или нескольких элементах.
48
+ * @function superellipseInit
49
+ * @memberof module:js-superellipse
50
+ * @param {string|Element|NodeList|Array<Element>} target - CSS-селектор, DOM-элемент или коллекция.
51
+ * @param {Object} [options] - Опции инициализации.
52
+ * @param {string} [options.mode='svg-layer'] - Режим: `'svg-layer'` (полная поддержка стилей) или `'clip-path'` (только обрезка).
53
+ * @param {number} [options.curveFactor] - Коэффициент кривизны (от -2 до 2). По умолчанию `(4/3)*(√2-1) ≈ 0.5523`.
54
+ * @param {number} [options.precision=2] - Количество знаков после запятой в координатах пути.
55
+ * @param {boolean} [options.force=false] - Принудительное пересоздание контроллера, если элемент уже инициализирован.
56
+ * @returns {SuperellipseController|Array<SuperellipseController>} Контроллер для одного элемента или массив контроллеров.
57
+ * @throws {Error} Если target не является селектором, элементом или коллекцией.
58
+ */
59
+
60
+ /**
61
+ * Класс контроллера, управляющего жизненным циклом суперэллипса для конкретного элемента.
62
+ * @class SuperellipseController
63
+ * @memberof module:js-superellipse
64
+ * @hideconstructor
65
+ * @example
66
+ * const controller = new SuperellipseController(element, options);
67
+ * // или через фабрику: element.superellipseInit(options)
68
+ */
69
+
70
+ (function (global, factory) {
71
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
72
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
73
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Superellipse = {}));
74
+ })(this, (function (exports) { 'use strict';
75
+
76
+ /**
77
+ * @file src/support.js
78
+ *
79
+ * @module sj-superellipse/support
80
+ * @since 1.0.0
81
+ * @author f4n70m
82
+ *
83
+ * @description
84
+ * Вспомогательные утилиты и инструменты отладки.
85
+ */
86
+
87
+
88
+ /**
89
+ * Объект для проверки поддержки CSS-селекторов.
90
+ * @namespace jsse_css_selector
91
+ * @since 1.1.0
92
+ */
93
+ const jsse_css_selector = {
94
+
95
+ /**
96
+ * Кэш результатов проверки поддержки селекторов.
97
+ * @since 1.1.0
98
+ * @type {Object<string, boolean>}
99
+ */
100
+ list : {},
101
+
102
+ /**
103
+ * Проверяет, поддерживает ли браузер указанный CSS-селектор.
104
+ * @since 1.1.0
105
+ * @param {string} selector - CSS-селектор для проверки (например, ':has(.a)').
106
+ * @returns {boolean} true, если селектор поддерживается, иначе false.
107
+ */
108
+ isSupport(selector) {
109
+ if (this.list[selector] === undefined) {
110
+ try {
111
+ const value = `selector(${selector})`;
112
+ this.list[selector] = CSS.supports(value);
113
+ } catch (e) {
114
+ this.list[selector] = false;
115
+ }
116
+ }
117
+ return this.list[selector];
118
+ }
119
+ };
120
+
121
+
122
+ /**
123
+ * Объект для управления отладочным выводом в консоль.
124
+ * @namespace jsse_console
125
+ * @since 1.0.0
126
+ */
127
+ const jsse_console = {
128
+
129
+ /**
130
+ * Список DOM-элементов, для которых включена отладка.
131
+ * @type {Element[]}
132
+ * @private
133
+ */
134
+ _list: [],
135
+
136
+ /**
137
+ * Включает отладку для указанного элемента.
138
+ * @since 1.0.0
139
+ * @param {Element} element - DOM-элемент.
140
+ * @returns {void}
141
+ */
142
+ set(element) {
143
+ this._list.push(element);
144
+ // this.debug('set', {element});
145
+ // console.log(this._list);
146
+ // console.debug(`[DEBUG]`, {src:'jsse_console::set', element});
147
+ console.debug('[JSSE]', '[DEBUG]', true, '\n\t', {element:element});
148
+ },
149
+
150
+ /**
151
+ * Выводит отладочное сообщение в консоль (если отладка включена для элемента или сообщение глобальное).
152
+ * @since 1.0.0
153
+ * @param {Object} options - Опции.
154
+ * @param {Element} [options.element] - Элемент, для которого проверяется включение отладки.
155
+ * @param {string} [options.label='DEBUG'] - Метка для вывода.
156
+ * @param {...any} values - Значения для вывода.
157
+ * @returns {void}
158
+ */
159
+ debug(options, ...values) {
160
+ if (options.element) {
161
+ if(this._list.includes(options.element)) {
162
+ console.debug('[JSSE]', `[${options?.label??'DEBUG'}]`, ...values, '\n\t', {element:options.element});
163
+ }
164
+ }
165
+ else {
166
+ console.debug('[JSSE]', `[${options?.label??'DEBUG'}]`, ...values);
167
+ }
168
+ },
169
+
170
+ /**
171
+ * Выводит предупреждение в консоль (если отладка включена для элемента или сообщение глобальное).
172
+ * @since 1.0.0
173
+ * @param {Object} options - Опции.
174
+ * @param {Element} [options.element] - Элемент, для которого проверяется включение отладки.
175
+ * @param {string} [options.label='DEBUG'] - Метка для вывода.
176
+ * @param {...any} values - Значения для вывода.
177
+ * @returns {void}
178
+ */
179
+ warn(options, ...values) {
180
+ if (options.element) {
181
+ if(this._list.includes(options.element)) {
182
+ console.warn('[JSSE]', `[${options?.label??'DEBUG'}]`, ...values, '\n\t', {element:options.element});
183
+ }
184
+ }
185
+ else {
186
+ console.warn('[JSSE]', `[${options?.label??'DEBUG'}]`, ...values);
187
+ }
188
+ }
189
+ };
190
+
191
+ /**
192
+ * @file src/support.js
193
+ *
194
+ * @module sj-superellipse/css-parser
195
+ * @since 1.1.0
196
+ * @author f4n70m
197
+ *
198
+ * @description
199
+ * Модуль парсинга css стилей страницы.
200
+ */
201
+
202
+
203
+
204
+ /**
205
+ * Представляет фрагмент CSS-селектора (часть между комбинаторами).
206
+ * @class StylesheetParserFragment
207
+ * @since 1.1.0
208
+ */
209
+ class StylesheetParserFragment {
210
+ _combinator;
211
+ _full;
212
+ _clean;
213
+ _pseudo;
214
+
215
+ /**
216
+ * @param {Object} options - Параметры фрагмента.
217
+ * @param {string} options.combinator - Комбинатор (пробел, '>', '+', '~').
218
+ * @param {string} options.full - Полный текст фрагмента.
219
+ * @param {string} options.clean - Очищенный текст (без псевдоклассов).
220
+ * @param {string[]} options.pseudo - Список псевдоклассов/псевдоэлементов.
221
+ */
222
+ constructor(options) {
223
+ this._combinator = options.combinator;
224
+ const isRoot = options.full === ':root';
225
+ this._clean = isRoot||options.clean?options.clean:`*`;
226
+ this._full = isRoot||options.clean?options.full:`*${options.full}`;
227
+ this._pseudo = [...new Set(options.pseudo)];
228
+ }
229
+
230
+ /**
231
+ * Возвращает комбинатор.
232
+ * @since 1.1.0
233
+ * @returns {string}
234
+ */
235
+ getCombinator() {
236
+ return this._combinator;
237
+ };
238
+
239
+ /**
240
+ * Возвращает полный текст фрагмента.
241
+ * @since 1.1.0
242
+ * @returns {string}
243
+ */
244
+ getFull() {
245
+ return this._full;
246
+ }
247
+
248
+ /**
249
+ * Возвращает очищенный текст фрагмента.
250
+ * @since 1.1.0
251
+ * @returns {string}
252
+ */
253
+ getClean() { return this._clean; };
254
+
255
+ /**
256
+ * Возвращает список псевдоклассов/псевдоэлементов.
257
+ * @since 1.1.0
258
+ * @returns {string[]}
259
+ */
260
+ getPseudoList() { return this.pseudo; };
261
+
262
+ /**
263
+ * Проверяет наличие конкретного псевдокласса.
264
+ * @since 1.1.0
265
+ * @param {string} pseudo - Псевдокласс (например, ':hover').
266
+ * @returns {boolean}
267
+ */
268
+ hasPseudo(pseudo) {
269
+ return this._pseudo.includes(pseudo);
270
+ }
271
+
272
+ /**
273
+ * Проверяет наличие псевдокласса `:hover`.
274
+ * @since 1.1.0
275
+ * @returns {boolean}
276
+ */
277
+ hasHover() {
278
+ return this.hasPseudo(':hover');
279
+ }
280
+ }
281
+
282
+
283
+ /**
284
+ * Список фрагментов селектора (массив с валидацией).
285
+ * @class StylesheetParserFragmentList
286
+ * @extends Array
287
+ * @since 1.1.0
288
+ */
289
+ class StylesheetParserFragmentList extends Array {
290
+
291
+
292
+ /**
293
+ * =============================================================
294
+ * ARRAY
295
+ * =============================================================
296
+ */
297
+
298
+
299
+ /**
300
+ * Проверяет, является ли элемент валидным фрагментом.
301
+ * @since 1.1.0
302
+ * @static
303
+ * @private
304
+ * @param {any} item - Проверяемый элемент.
305
+ * @returns {boolean}
306
+ */
307
+ static #isValid(item) {
308
+ return item instanceof StylesheetParserFragment;
309
+ }
310
+
311
+ /**
312
+ * Добавляет валидные фрагменты в конец массива.
313
+ * @override
314
+ * @param {...StylesheetParserFragment} items - Фрагменты.
315
+ * @returns {number}
316
+ */
317
+ push(...items) {
318
+ const valid = items.filter(item => StylesheetParserFragmentList.#isValid(item));
319
+ return super.push(...valid);
320
+ }
321
+
322
+ /**
323
+ * Добавляет валидные фрагменты в начало массива.
324
+ * @override
325
+ * @param {...StylesheetParserFragment} items - Фрагменты.
326
+ * @returns {number}
327
+ */
328
+ unshift(...items) {
329
+ const valid = items.filter(item => StylesheetParserFragmentList.#isValid(item));
330
+ return super.unshift(...valid);
331
+ }
332
+
333
+ /**
334
+ * Изменяет содержимое массива, удаляя или заменяя элементы.
335
+ * @override
336
+ * @param {number} start - Индекс начала.
337
+ * @param {number} deleteCount - Количество удаляемых элементов.
338
+ * @param {...StylesheetParserFragment} items - Добавляемые фрагменты.
339
+ * @returns {StylesheetParserFragment[]}
340
+ */
341
+ splice(start, deleteCount, ...items) {
342
+ const valid = items.filter(item => StylesheetParserFragmentList.#isValid(item));
343
+ return super.splice(start, deleteCount, ...valid);
344
+ }
345
+
346
+ // Все остальные методы (forEach, map, filter, find, pop, shift и т.д.)
347
+ // работают автоматически, так как они наследуются от Array
348
+
349
+
350
+ /**
351
+ * =============================================================
352
+ * PUBLIC
353
+ * =============================================================
354
+ */
355
+
356
+
357
+ /**
358
+ * Возвращает последний фрагмент (целевой).
359
+ * @since 1.1.0
360
+ * @returns {StylesheetParserFragment}
361
+ */
362
+ getTarget() {
363
+ return this[this.length-1];
364
+ }
365
+
366
+ /**
367
+ * Возвращает индексы фрагментов, содержащих `:hover`.
368
+ * @since 1.1.0
369
+ * @returns {number[]}
370
+ */
371
+ getTriggerIndexList() {
372
+ return this.reduce((indexes, element, index) => {
373
+ if (element.hasHover()) {
374
+ indexes.push(index);
375
+ }
376
+ return indexes;
377
+ }, []);
378
+ }
379
+
380
+ /**
381
+ * Проверяет, есть ли хотя бы один фрагмент с `:hover`.
382
+ * @since 1.1.0
383
+ * @returns {boolean}
384
+ */
385
+ hasTrigger() {
386
+ return this.getTriggerIndexList().length > 0;
387
+ }
388
+
389
+ /**
390
+ * Проверяет, содержит ли целевой фрагмент `:hover`.
391
+ * @since 1.1.0
392
+ * @returns {boolean}
393
+ */
394
+ targetIsTriggered() {
395
+ return this.getTarget().hasHover();
396
+ }
397
+
398
+
399
+ /**
400
+ * =============================================================
401
+ * PRIVATE
402
+ * =============================================================
403
+ */
404
+ }
405
+
406
+
407
+ /**
408
+ * Представляет CSS-правило (селектор + стили).
409
+ * @class StylesheetParserSelector
410
+ * @since 1.1.0
411
+ */
412
+ class StylesheetParserSelector {
413
+
414
+ _media;
415
+ _selector;
416
+ _fragments;
417
+
418
+ _ruleStyle;
419
+ _styles;
420
+
421
+ _triggerFragments;
422
+ _triggerIndexList;
423
+ _triggerParts;
424
+
425
+ /**
426
+ * @param {string} selector - Текст селектора.
427
+ * @param {CSSStyleDeclaration} ruleStyle - Стили правила.
428
+ * @param {string|false} media - Медиа-выражение (если правило внутри @media).
429
+ */
430
+ constructor(selector, ruleStyle, media) {
431
+ /** @type {string|false} */
432
+ this._media = media;
433
+ /** @type {string} */
434
+ this._selector = selector;
435
+ /** @type {StylesheetParserFragmentList|null} */
436
+ this._fragments = null;
437
+ /** @type {CSSStyleDeclaration} */
438
+ this._ruleStyle = ruleStyle;
439
+ /** @type {Object<string, string>|null} */
440
+ this._styles = null;
441
+ /** @type {StylesheetParserFragmentList|null} */
442
+ this._triggerFragments = null;
443
+ /** @type {number[]|null} */
444
+ this._triggerIndexList = null;
445
+ /** @type {Array|null} */
446
+ this._triggerParts = null;
447
+ }
448
+
449
+ /**
450
+ * =============================================================
451
+ * PUBLIC
452
+ * =============================================================
453
+ */
454
+
455
+
456
+ /**
457
+ * Возвращает текст селектора.
458
+ * @since 1.1.0
459
+ * @returns {string}
460
+ */
461
+ getSelector() {
462
+ return this._selector;
463
+ }
464
+
465
+ /**
466
+ * Возвращает распарсенные стили правила.
467
+ * @since 1.1.0
468
+ * @returns {Object<string, string>}
469
+ */
470
+ getStyles() {
471
+ if (this._styles === null) {
472
+ this._styles = this._parseStyles();
473
+ }
474
+ return this._styles;
475
+ }
476
+
477
+ /**
478
+ * Возвращает список фрагментов селектора.
479
+ * @since 1.1.0
480
+ * @returns {StylesheetParserFragmentList}
481
+ */
482
+ getFragments() {
483
+ if (this._fragments === null) {
484
+ this._fragments = this._getSelectorFragments(this._selector);
485
+ }
486
+ return this._fragments;
487
+ }
488
+
489
+ /**
490
+ * Проверяет, соответствует ли текущее медиа-правило.
491
+ * @since 1.1.0
492
+ * @returns {boolean}
493
+ */
494
+ matchMedia() {
495
+ return !this._media || window.matchMedia(this._media).matches;
496
+ }
497
+
498
+ /**
499
+ * Возвращает части селектора, участвующие в hover-триггерах.
500
+ * @since 1.1.0
501
+ * @returns {Array<{parent: string, neighbor: Object|null, child: string}>}
502
+ */
503
+ getTriggerParts() {
504
+ if (this._triggerParts === null) {
505
+ this._triggerParts = [];
506
+ const fragments = this.getFragments();
507
+ const targetIndexList = fragments.getTriggerIndexList();
508
+ for(const targetIndex of targetIndexList) {
509
+ const parts = {parent:'',neighbor:null,child:null};
510
+ for (let i = 0; i<fragments.length; i++) {
511
+ const fragment = fragments[i];
512
+ if (i <= targetIndex) {
513
+ parts.parent += fragment.getCombinator() + fragment.getClean();
514
+
515
+ if (i + 1 < fragments.length) {
516
+ const nextFragment = fragments[i+1];
517
+ const nextCombinator = nextFragment.getCombinator();
518
+ if ([' + ', ' ~ '].includes(nextCombinator)) {
519
+ parts.neighbor = {
520
+ combinator: nextCombinator,
521
+ clean : nextFragment.getClean()
522
+ };
523
+ i++;
524
+ }
525
+ }
526
+ } else {
527
+ if (!parts.child) parts.child = '';
528
+ parts.child += fragment.getCombinator() + fragment.getClean();
529
+ }
530
+ }
531
+ this._triggerParts.push(parts);
532
+ }
533
+ }
534
+ return this._triggerParts;
535
+ }
536
+
537
+ /**
538
+ * Возвращает фрагменты, содержащие `:hover`.
539
+ * @since 1.1.0
540
+ * @returns {StylesheetParserFragmentList}
541
+ */
542
+ getTriggerFragments() {
543
+ if (this._triggerFragments === null) {
544
+ this._triggerFragments = new StylesheetParserFragmentList();
545
+ const fragments = this.getFragments();
546
+ const indexList = fragments.getTriggerIndexList();
547
+ for(const index of indexList) {
548
+ const fragment = fragments[index];
549
+ this._triggerFragments.push(fragment);
550
+ }
551
+ }
552
+ return this._triggerFragments;
553
+ }
554
+
555
+
556
+
557
+ /**
558
+ * =============================================================
559
+ * PRIVATE
560
+ * =============================================================
561
+ */
562
+
563
+ /**
564
+ * Парсит CSS-стили из cssText.
565
+ * @private
566
+ * @returns {Object<string, string>}
567
+ */
568
+ _parseStyles() {
569
+ const styles = {};
570
+ if (this._ruleStyle.cssText) {
571
+ const declarations = this._ruleStyle.cssText.split(';');
572
+ for (const decl of declarations) {
573
+ const colonIndex = decl.indexOf(':');
574
+ if (colonIndex > 0) {
575
+ const prop = decl.substring(0, colonIndex).trim();
576
+ const value = decl.substring(colonIndex + 1).trim();
577
+ if (prop && value) {
578
+ styles[prop] = value;
579
+ }
580
+ }
581
+ }
582
+ }
583
+ return styles;
584
+ }
585
+
586
+ /**
587
+ * Разбивает селектор на фрагменты.
588
+ * @private
589
+ * @param {string} selector - Текст селектора.
590
+ * @returns {StylesheetParserFragmentList}
591
+ */
592
+ _getSelectorFragments(selector) {
593
+ const s = selector;
594
+ const result = new StylesheetParserFragmentList();
595
+ let options = {
596
+ combinator : '',
597
+ full : '',
598
+ clean : '',
599
+ pseudo: []
600
+ };
601
+ let pseudo = '';
602
+ let i = 0;
603
+ while (i < s.length) {
604
+ if (s[i] === ':') {
605
+ /** завершение записи текущего фрагмента перед псевдоэлементом **/
606
+ if (i > 0 && s[i + 1] === ':') {
607
+
608
+ if (options.combinator === '') options.combinator = ' ';
609
+
610
+ result.push(new StylesheetParserFragment(options));
611
+ options = {
612
+ combinator : '',
613
+ full : '',
614
+ clean : '',
615
+ pseudo: []
616
+ };
617
+ }
618
+ /** Псевдоэлемент (два двоеточия) **/
619
+ if (s[i + 1] === ':' || (i > 0 && s[i - 1] === ':')) {
620
+ options.full += s[i];
621
+ options.clean += s[i];
622
+ i++;
623
+ }
624
+ /** Псевдокласс **/
625
+ else {
626
+
627
+ pseudo += s[i];
628
+ let j = i + 1;
629
+ // pseudo += s[i];
630
+ while (j < s.length && /[\w-]/.test(s[j])) {
631
+ pseudo += s[j];
632
+ j++;
633
+ }
634
+ // если есть аргументы в скобках
635
+ if (s[j] === '(') {
636
+ pseudo += s[j];
637
+ let depth = 1, k = j + 1;
638
+ while (k < selector.length && depth) {
639
+ if (selector[k] === '(') depth++;
640
+ else if (selector[k] === ')') depth--;
641
+ pseudo += s[k];
642
+ k++;
643
+ }
644
+ options.pseudo.push(pseudo);
645
+ options.full += pseudo;
646
+ pseudo = '';
647
+ i = k;
648
+ }
649
+ // если
650
+ else {
651
+ options.pseudo.push(pseudo);
652
+ options.full += pseudo;
653
+ pseudo = '';
654
+ i = j;
655
+ }
656
+ }
657
+ } else {
658
+ /** проверка combinator **/
659
+ if (s[i] === ' ') {
660
+
661
+ result.push(new StylesheetParserFragment(options));
662
+
663
+ let combinator = ' ';
664
+ if (/[\>\~\+]/.test(s[i+1]) && s[i + 2] === ' ') {
665
+ combinator = s[i]+s[i+1]+s[i+2];
666
+ i += 2;
667
+ }
668
+
669
+ options = {
670
+ combinator : combinator,
671
+ full : '',
672
+ clean : '',
673
+ pseudo: []
674
+ };
675
+
676
+ i++;
677
+ }
678
+ else {
679
+ options.full += s[i];
680
+ options.clean += s[i];
681
+ i++;
682
+ }
683
+ }
684
+ }
685
+ result.push(new StylesheetParserFragment(options));
686
+ return result;
687
+ }
688
+ }
689
+
690
+
691
+ /**
692
+ * Список CSS-селекторов (массив с валидацией).
693
+ * @class StylesheetParserSelectorList
694
+ * @extends Array
695
+ * @since 1.1.0
696
+ */
697
+ class StylesheetParserSelectorList extends Array {
698
+
699
+
700
+ /**
701
+ * =============================================================
702
+ * ARRAY
703
+ * =============================================================
704
+ */
705
+
706
+
707
+ /**
708
+ * Проверяет, является ли элемент валидным селектором.
709
+ * @since 1.1.0
710
+ * @static
711
+ * @private
712
+ * @param {any} item - Проверяемый элемент.
713
+ * @returns {boolean}
714
+ */
715
+ static #isValid(item) {
716
+ return item instanceof StylesheetParserSelector;
717
+ }
718
+
719
+ /**
720
+ * Добавляет валидные селекторы в конец массива.
721
+ * @since 1.1.0
722
+ * @override
723
+ * @param {...StylesheetParserSelector} items - Селекторы.
724
+ * @returns {number}
725
+ */
726
+ push(...items) {
727
+ const valid = items.filter(item => StylesheetParserSelectorList.#isValid(item));
728
+ return super.push(...valid);
729
+ }
730
+
731
+ /**
732
+ * Добавляет валидные селекторы в начало массива.
733
+ * @since 1.1.0
734
+ * @override
735
+ * @param {...StylesheetParserSelector} items - Селекторы.
736
+ * @returns {number}
737
+ */
738
+ unshift(...items) {
739
+ const valid = items.filter(item => StylesheetParserSelectorList.#isValid(item));
740
+ return super.unshift(...valid);
741
+ }
742
+
743
+ /**
744
+ * Изменяет содержимое массива, удаляя или заменяя элементы.
745
+ * @since 1.1.0
746
+ * @override
747
+ * @param {number} start - Индекс начала.
748
+ * @param {number} deleteCount - Количество удаляемых элементов.
749
+ * @param {...StylesheetParserSelector} items - Добавляемые селекторы.
750
+ * @returns {StylesheetParserSelector[]}
751
+ */
752
+ splice(start, deleteCount, ...items) {
753
+ const valid = items.filter(item => StylesheetParserSelectorList.#isValid(item));
754
+ return super.splice(start, deleteCount, ...valid);
755
+ }
756
+
757
+ // Все остальные методы (forEach, map, filter, find, pop, shift и т.д.)
758
+ // работают автоматически, так как они наследуются от Array
759
+
760
+
761
+ /**
762
+ * =============================================================
763
+ * PUBLIC
764
+ * =============================================================
765
+ */
766
+
767
+
768
+ /**
769
+ * Возвращает селекторы, содержащие `:hover`.
770
+ * @since 1.1.0
771
+ * @returns {StylesheetParserSelector[]}
772
+ */
773
+ getSelectorsWithHover() {
774
+ return this.filter(item => item.getFragments().hasTrigger());
775
+ }
776
+
777
+
778
+ /**
779
+ * =============================================================
780
+ * PRIVATE
781
+ * =============================================================
782
+ */
783
+ }
784
+
785
+
786
+ /**
787
+ * Парсер таблиц стилей. Извлекает CSS-правила, разбирает селекторы и предоставляет доступ к ним.
788
+ * @class StylesheetParser
789
+ * @since 1.1.0
790
+ */
791
+ class StylesheetParser {
792
+ _selectors;
793
+ _isParsed;
794
+
795
+ constructor() {
796
+ /** @type {StylesheetParserSelectorList|null} */
797
+ this._selectors = null;
798
+ /** @type {boolean} */
799
+ this._isParsed = false;
800
+ }
801
+
802
+ /**
803
+ * =============================================================
804
+ * PUBLIC
805
+ * =============================================================
806
+ */
807
+
808
+ /**
809
+ * Возвращает список всех селекторов.
810
+ * @since 1.1.0
811
+ * @returns {StylesheetParserSelectorList}
812
+ */
813
+ getSelectors() {
814
+ this._ensureParsed();
815
+ return this._selectors;
816
+ }
817
+
818
+ /**
819
+ * Возвращает селекторы, содержащие `:hover`.
820
+ * @since 1.1.0
821
+ * @returns {StylesheetParserSelector[]}
822
+ */
823
+ getSelectorsHasHover() {
824
+ this._ensureParsed();
825
+ return this._selectors.getSelectorsWithHover();
826
+ }
827
+
828
+ /**
829
+ * Возвращает селекторы, которые соответствуют элементу и (опционально) содержат `:hover`.
830
+ * @since 1.1.0
831
+ * @param {Element} element - Целевой элемент.
832
+ * @param {Object} [options] - Опции.
833
+ * @param {boolean} [options.selectorHasHover] - Если true, возвращать только селекторы с `:hover`.
834
+ * @returns {StylesheetParserSelector[]}
835
+ */
836
+ getTargetSelectors(element, options={}) {
837
+ this._ensureParsed();
838
+ const targetList = this._createList();
839
+ const parserList = this.getSelectors();
840
+ for(const selector of parserList) {
841
+ const fragments = selector.getFragments();
842
+ const clean = fragments.getTarget().getClean();
843
+ if (!clean) continue;
844
+ const isMatch = element.matches(clean);
845
+ fragments.hasTrigger();
846
+ if (
847
+ isMatch &&
848
+ ( !options?.selectorHasHover || options.selectorHasHover && fragments.hasTrigger() )
849
+ ) {
850
+ targetList.push(selector);
851
+ }
852
+ }
853
+ return targetList;
854
+ }
855
+
856
+ /**
857
+ * Сбрасывает состояние парсера (принудительный перепарсинг при следующем вызове).
858
+ * @since 1.1.0
859
+ * @returns {void}
860
+ */
861
+ reset() {
862
+ this._selectors = null;
863
+ this._isParsed = false;
864
+ }
865
+
866
+ /**
867
+ * =============================================================
868
+ * PRIVATE
869
+ * =============================================================
870
+ */
871
+
872
+
873
+ /**
874
+ * Гарантирует, что парсинг выполнен.
875
+ * @private
876
+ * @returns {void}
877
+ */
878
+ _ensureParsed() {
879
+ if (this._isParsed) return;
880
+ this._init();
881
+ this._isParsed = true;
882
+ }
883
+
884
+ /**
885
+ * Инициализирует парсер: создаёт список и парсит CSS-правила.
886
+ * @private
887
+ * @returns {void}
888
+ */
889
+ _init() {
890
+ this._selectors = this._createList();
891
+ this._parseCssRules();
892
+ jsse_console.debug({label:'STYLESHEET'}, '[LOADED]');
893
+ }
894
+
895
+ /**
896
+ * Создаёт пустой список селекторов.
897
+ * @private
898
+ * @returns {StylesheetParserSelectorList}
899
+ */
900
+ _createList() {
901
+ return new StylesheetParserSelectorList();
902
+ }
903
+
904
+ /**
905
+ * Парсит все CSS-правила из document.styleSheets.
906
+ * @private
907
+ * @returns {void}
908
+ */
909
+ _parseCssRules() {
910
+ for (const styleSheet of document.styleSheets) {
911
+ try {
912
+ const rules = styleSheet.cssRules || styleSheet.rules;
913
+ if (!rules) continue;
914
+
915
+ for (const rule of rules) {
916
+ if (rule.type === CSSRule.STYLE_RULE) {
917
+ this._selectors.push(...this._getParsedCssRule(rule));
918
+ }
919
+ else if (rule.type === CSSRule.MEDIA_RULE) {
920
+ try {
921
+ for (const subRule of rule.cssRules) {
922
+ this._selectors.push(...this._getParsedCssRule(subRule, rule.media.mediaText));
923
+ }
924
+ } catch (e) {
925
+ jsse_console.warn({label:'STYLESHEET'}, `Error processing @media rules from ${styleSheet.href || 'inline'}:`, e.message);
926
+ }
927
+ }
928
+ // Можно добавить другие типы правил (CSSRule.IMPORT_RULE, CSSRule.SUPPORTS_RULE и т.д.)
929
+ }
930
+ } catch (e) {
931
+ if (e.name === 'SecurityError') {
932
+ jsse_console.warn(
933
+ {label:'STYLESHEET'},
934
+ `Cannot access stylesheet rules:`,
935
+ `\n${styleSheet.href || 'inline / blob'}.`,
936
+ `\nCause: CORS or file:// protocol.`,
937
+ `\nTo fix this, use a local server (http://) or add the crossorigin attribute.`
938
+ );
939
+ } else if (e.name === 'InvalidAccessError') {
940
+ jsse_console.warn({label:'STYLESHEET'}, `The stylesheet ${styleSheet.href || 'inline'} has not yet loaded or has invalid access.`);
941
+ } else {
942
+ jsse_console.warn({label:'STYLESHEET'}, `Unknown error reading ${styleSheet.href || 'inline'}:`, e.message);
943
+ }
944
+ }
945
+ }
946
+ }
947
+
948
+ /**
949
+ * Преобразует CSSRule в массив селекторов.
950
+ * @private
951
+ * @param {CSSRule} rule - CSS-правило.
952
+ * @param {string|false} [media=false] - Медиа-выражение (если правило внутри @media).
953
+ * @returns {StylesheetParserSelector[]}
954
+ */
955
+ _getParsedCssRule(rule, media=false) {
956
+ const result = [];
957
+ const selectorGroup = rule.selectorText;
958
+ const selectorList = this._splitSelectorGroup(selectorGroup);
959
+ const uniqueSelectorList = [...new Set(selectorList)];
960
+ for (const selector of uniqueSelectorList) {
961
+ result.push( new StylesheetParserSelector(selector, rule.style, media) );
962
+ }
963
+ return result;
964
+ }
965
+
966
+
967
+ /**
968
+ * Разбивает группу селекторов по запятым, игнорируя запятые внутри скобок.
969
+ * @private
970
+ * @param {string} selectorText - Текст группы селекторов.
971
+ * @returns {string[]}
972
+ * @example
973
+ * _splitSelectorGroup(":not(.a, .b), .c") → [":not(.a, .b)", ".c"]
974
+ */
975
+ _splitSelectorGroup(selectorText) {
976
+ const result = [];
977
+ let current = '';
978
+ let depth = 0; // глубина вложенности в круглые скобки
979
+ let inString = false; // для простоты не поддерживаем строки (в CSS их почти нет в селекторах)
980
+ let escape = false;
981
+
982
+ for (let i = 0; i < selectorText.length; i++) {
983
+ const ch = selectorText[i];
984
+
985
+ if (escape) {
986
+ current += ch;
987
+ escape = false;
988
+ continue;
989
+ }
990
+
991
+ if (ch === '\\') {
992
+ escape = true;
993
+ current += ch;
994
+ continue;
995
+ }
996
+
997
+ if (ch === '"' || ch === "'") {
998
+ inString = !inString;
999
+ current += ch;
1000
+ continue;
1001
+ }
1002
+
1003
+ if (!inString) {
1004
+ if (ch === '(') {
1005
+ depth++;
1006
+ } else if (ch === ')') {
1007
+ depth--;
1008
+ }
1009
+ }
1010
+
1011
+ if (ch === ',' && depth === 0 && !inString) {
1012
+ // Запятая на нулевом уровне – разделитель
1013
+ result.push(current.trim());
1014
+ current = '';
1015
+ } else {
1016
+ current += ch;
1017
+ }
1018
+ }
1019
+
1020
+ if (current.trim() !== '') {
1021
+ result.push(current.trim());
1022
+ }
1023
+
1024
+ return result;
1025
+ }
1026
+ }
1027
+
1028
+ /**
1029
+ * @file src/global-cache.js
1030
+ *
1031
+ * @module sj-superellipse/global-cache
1032
+ * @since 1.0.0
1033
+ * @author f4n70m
1034
+ *
1035
+ * @description
1036
+ * Глобальные хранилища для кэширования данных между экземплярами контроллеров и режимов.
1037
+ */
1038
+
1039
+
1040
+
1041
+ /**
1042
+ * Карта для хранения контроллеров суперэллипса, связанных с DOM-элементами.
1043
+ * @since 1.0.0
1044
+ * @type {WeakMap<Element, import('./controller.js').SuperellipseController>}
1045
+ */
1046
+ const jsse_controllers = new WeakMap();
1047
+
1048
+
1049
+ /**
1050
+ * Экземпляр парсера таблиц стилей для анализа CSS-правил.
1051
+ * @since 1.1.0
1052
+ * @type {import('./stylesheet-parser.js').StylesheetParser}
1053
+ */
1054
+ const jsse_stylesheet = new StylesheetParser();
1055
+
1056
+
1057
+ /**
1058
+ * WeakMap для кэширования стилей элементов.
1059
+ * @since 1.0.0
1060
+ * @type {WeakMap<Element, Object>}
1061
+ */
1062
+ const jsse_styles = new WeakMap();
1063
+
1064
+
1065
+ /**
1066
+ * Объект для хранения глобальных CSS-правил режимов.
1067
+ * @since 1.0.0
1068
+ * @namespace jsse_reset_css
1069
+ */
1070
+ const jsse_reset_css = {
1071
+
1072
+ /**
1073
+ * Внутреннее хранилище элементов `<style>` для каждого режима.
1074
+ * @type {Object<string, {element: HTMLStyleElement, count: number}>}
1075
+ */
1076
+ _list: {},
1077
+
1078
+ /**
1079
+ * Возвращает запись для заданного ключа.
1080
+ * @since 1.0.0
1081
+ * @param {string} key - Идентификатор режима (например, 'svg-layer').
1082
+ * @returns {{element: HTMLStyleElement, count: number} | undefined}
1083
+ */
1084
+ get(key) {
1085
+ return this._list[key];
1086
+ },
1087
+
1088
+ /**
1089
+ * Сохраняет элемент `<style>` для режима, удаляя предыдущий при необходимости.
1090
+ * @since 1.0.0
1091
+ * @param {string} key - Идентификатор режима.
1092
+ * @param {HTMLStyleElement} el - Элемент стилей для добавления в `<head>`.
1093
+ * @returns {void}
1094
+ */
1095
+ set(key, el) {
1096
+ if (this.has(key)) {
1097
+ this.unset(key);
1098
+ }
1099
+ this._list[key] = {
1100
+ element: el,
1101
+ count: 1
1102
+ };
1103
+ /** Добавить элемент в конец <head> **/
1104
+ document.head.appendChild(el);
1105
+ },
1106
+
1107
+ /**
1108
+ * Удаляет запись и соответствующий элемент `<style>` из DOM.
1109
+ * @since 1.0.0
1110
+ * @param {string} key - Идентификатор режима.
1111
+ * @returns {void}
1112
+ */
1113
+ unset(key) {
1114
+ /** Удалить элемент **/
1115
+ this._list[key].element.remove();
1116
+ delete this._list[key];
1117
+ },
1118
+
1119
+ /**
1120
+ * Проверяет существование записи для указанного ключа.
1121
+ * @since 1.0.0
1122
+ * @param {string} key - Идентификатор режима.
1123
+ * @returns {boolean}
1124
+ */
1125
+ has(key) {
1126
+ return this._list[key] !== undefined;
1127
+ }
1128
+ };
1129
+
1130
+ /**
1131
+ * Счётчик для генерации уникальных идентификаторов контроллеров.
1132
+ * @since 1.0.0
1133
+ * @namespace jsse_counter
1134
+ */
1135
+ const jsse_counter = {
1136
+
1137
+ /**
1138
+ * Текущее значение счётчика.
1139
+ * @type {number}
1140
+ */
1141
+ _value: 0,
1142
+
1143
+ /**
1144
+ * Увеличивает счётчик на 1.
1145
+ * @returns {void}
1146
+ */
1147
+ increment() { this._value++; },
1148
+
1149
+ /**
1150
+ * Уменьшает счётчик на 1.
1151
+ * @returns {void}
1152
+ */
1153
+ decrement() { this._value--; },
1154
+
1155
+ /**
1156
+ * Геттер, возвращающий текущее значение счётчика.
1157
+ * @type {number}
1158
+ */
1159
+ get value() { return this._value; }
1160
+ };
1161
+
1162
+ /**
1163
+ * @file src/core.js
1164
+ *
1165
+ * @module sj-superellipse/core
1166
+ * @since 1.0.0
1167
+ * @author f4n70m
1168
+ *
1169
+ * @description
1170
+ * Ядро математических расчётов суперэллипса. Содержит функцию `jsse_generateSuperellipsePath`,
1171
+ * которая по заданным ширине, высоте, радиусу скругления и коэффициенту кривизны генерирует
1172
+ * SVG-путь, аппроксимирующий суперэллипс с помощью кубических кривых Безье.
1173
+ *
1174
+ * Также экспортирует вспомогательные функции:
1175
+ * - `jsse_roundf` – округление чисел с заданной точностью,
1176
+ * - `jsse_getBorderRadiusFactor` – возвращает константу (4/3)*(√2-1) для аппроксимации окружности.
1177
+ *
1178
+ * @see {@link https://en.wikipedia.org/wiki/Superellipse|Суперэллипс}
1179
+ * @see {@link https://github.com/f4n70m/js-superellipse|f4n70m/sj-superellipse}
1180
+ *
1181
+ * @example
1182
+ * import { jsse_generateSuperellipsePath } from 'js-superellipse/core';
1183
+ * const path = jsse_generateSuperellipsePath(200, 150, 30, 1.2);
1184
+ * svgPathElement.setAttribute('d', path);
1185
+ */
1186
+
1187
+ /**
1188
+ * Округляет число до заданного количества знаков после запятой.
1189
+ *
1190
+ * @since 1.0.0
1191
+ * @param {number} value - Исходное число.
1192
+ * @param {number} [precision=2] - Количество знаков после запятой (по умолчанию 2).
1193
+ * @returns {number} Округлённое число.
1194
+ */
1195
+ function jsse_roundf(value, precision = 2) {
1196
+ const factor = 10 ** precision;
1197
+ return (Math.round(value * factor) / factor).toFixed(precision);
1198
+ }
1199
+
1200
+ /**
1201
+ * Вычисляет параметр D2 для второй кривой Безье, обеспечивая касание
1202
+ * центра первой кривой с центром второй.
1203
+ *
1204
+ * В контексте суперэллипса параметры L (размах) и D (отклонение контрольных точек)
1205
+ * описывают форму кубической кривой Безье, используемой для построения углов.
1206
+ * Зная эталонную пару (L1, D1) и желаемый размах L2, функция находит такое D2,
1207
+ * при котором центральная точка второй кривой будет лежать на касательной,
1208
+ * проведённой из центра первой кривой (обеспечивается гладкое соединение).
1209
+ *
1210
+ * @since 1.0.0
1211
+ * @param {number} L1 - Размах эталонной кривой Безье.
1212
+ * @param {number} D1 - Отклонение контрольных точек эталонной кривой.
1213
+ * @param {number} L2 - Размах целевой кривой Безье.
1214
+ * @returns {number} Вычисленное значение D2, ограниченное сверху 1.
1215
+ * @throws {Error} Если L2 близко к нулю.
1216
+ */
1217
+ function jsse_getD2FromL1D1L2(L1, D1, L2) {
1218
+ if (Math.abs(L2) < 1e-10) {
1219
+ throw new Error('L2 не может быть нулевым');
1220
+ }
1221
+ const D2 = (4 * L2 - L1 * (4 - 3 * D1)) / (3 * L2);
1222
+ return Math.min(D2, 1);
1223
+ }
1224
+
1225
+ /**
1226
+ * @since 1.0.0
1227
+ */
1228
+ function jsse_getBorderRadiusFactor() {
1229
+ return (4 / 3) * (Math.sqrt(2) - 1);
1230
+ }
1231
+
1232
+ /**
1233
+ *
1234
+ * Основная функция генерации SVG path для суперэллипса с изменяемой формой углов.
1235
+ * Форма углов определяется параметром curveFactor (от -2 до 2):
1236
+ * -2 : вогнутые прямоугольные углы
1237
+ * -G : вогнутые круглые углы (G = (4/3)*(√2-1) ≈ 0.5523)
1238
+ * 0 : прямой скос
1239
+ * G : выпуклые круглые углы
1240
+ * 1 : выгнутые суперэллипсные углы
1241
+ * 2 : выгнутые прямоугольные углы
1242
+ *
1243
+ * Функция генерирует непрерывный спектр форм от вогнутых прямоугольных (-2) до выгнутых прямоугольных (2), проходя через скос (0), круглые углы (±G) и суперэллипсные (1);
1244
+ * где G = (4/3)*(√2-1) ≈ 0.5522847498 — константа, при которой кривые Безье аппроксимируют четверть окружности.
1245
+ * Благодаря использованию кубических кривых Безье и тщательно подобранной интерполяции достигается плавное изменение геометрии при любом curveFactor в заданном диапазоне.
1246
+ *
1247
+ * @since 1.0.0
1248
+ * @param {number} width - Ширина фигуры.
1249
+ * @param {number} height - Высота фигуры.
1250
+ * @param {number} radius - Радиус скругления углов (будет автоматически ограничен).
1251
+ * @param {number} curveFactor - Коэффициент формы углов, диапазон [-2, 2].
1252
+ * @param {number} [precision=2] - Количество знаков после запятой в координатах.
1253
+ * @returns {string} Строка с SVG-командами для элемента <path>.
1254
+ * @throws {Error} Если при вычислениях возникает деление на ноль (маловероятно при корректных параметрах).
1255
+ *
1256
+ */
1257
+ function jsse_generateSuperellipsePath(width, height, radius, curveFactor, precision = 2) {
1258
+ if (width <= 0 || height <= 0) {
1259
+ return "M0,0"; // или "M0,0" – пустой путь
1260
+ }
1261
+ if (typeof radius !== 'number' || isNaN(radius)) {
1262
+ radius = 0;
1263
+ }
1264
+ /** константа идеальной окружности **/
1265
+ const G = jsse_getBorderRadiusFactor();
1266
+ /** константа максимальной L при D == 1 **/
1267
+ const J = 8 - 4 * Math.sqrt(2);
1268
+
1269
+ /** Множитель для нормализации координат **/
1270
+ let M = width >= height ? width : height;
1271
+
1272
+ let kValue = Math.abs(curveFactor);
1273
+ let kSign = curveFactor >= 0 ? 1 : -1;
1274
+
1275
+ /** Ограничиваем радиус половиной меньшей стороны **/
1276
+ let rxMax = width / 2;
1277
+ let ryMax = height / 2;
1278
+ let rMax = Math.min(rxMax, ryMax);
1279
+ let r = Math.min(radius, rMax);
1280
+ let rMaxSpan = Math.abs(rxMax - ryMax);
1281
+ if (rxMax >= ryMax) {
1282
+ rxMax = ryMax + Math.min(rMax / 4, rMaxSpan / 4);
1283
+ } else {
1284
+ ryMax = rxMax + Math.min(rMax / 4, rMaxSpan / 4);
1285
+ }
1286
+ let rx = Math.min(r, rxMax);
1287
+ let ry = Math.min(r, ryMax);
1288
+
1289
+ let Dx = 0, Dy = 0;
1290
+ let Lx = 0, Ly = 0;
1291
+ let Sx = 0, Sy = 0;
1292
+
1293
+ if (r !== 0) {
1294
+ let R = 1;
1295
+
1296
+ /** Определить эллипсные (k=1) **/
1297
+ let Rk1x = rxMax / rx;
1298
+ let Rk1y = ryMax / ry;
1299
+ let Lk1x = (kSign > 0) ? Math.min(Rk1x, J) : 1;
1300
+ let Lk1y = (kSign > 0) ? Math.min(Rk1y, J) : 1;
1301
+ jsse_getD2FromL1D1L2(R, G, Lk1x);
1302
+ jsse_getD2FromL1D1L2(R, G, Lk1y);
1303
+ let Jk1x = (1 / J) * Lk1x;
1304
+ let Jk1y = (1 / J) * Lk1y;
1305
+
1306
+ /** Относительное L (от 1 до Lk1, при k от G до 1) **/
1307
+ let Lk = Math.max((Math.min(kValue, 1) - G) / (1 - G), 0);
1308
+ let Lix = 1 + (Lk1x - 1) * Lk;
1309
+ let Liy = 1 + (Lk1y - 1) * Lk;
1310
+
1311
+ /** Определить Di (от Li) **/
1312
+ let Six = 0, Siy = 0;
1313
+ let Dix, Diy;
1314
+ if (kValue <= G) {
1315
+ Dix = Diy = kValue;
1316
+ } else {
1317
+ let Dlix = jsse_getD2FromL1D1L2(R, G, Lix);
1318
+ let Dliy = jsse_getD2FromL1D1L2(R, G, Liy);
1319
+ if (kValue <= 1) {
1320
+ Dix = Dlix;
1321
+ Diy = Dliy;
1322
+ } else {
1323
+ Jk1x = G + (1 / (Lk1x / J)) * (1 - G);
1324
+ Jk1y = G + (1 / (Lk1y / J)) * (1 - G);
1325
+ let Jix = Math.min((kValue - 1) / (Jk1x - 1), 1);
1326
+ let Jiy = Math.min((kValue - 1) / (Jk1y - 1), 1);
1327
+
1328
+ let Lsx = Lix + (J - Lix) * Jix;
1329
+ let Lsy = Liy + (J - Liy) * Jiy;
1330
+ let Dlsx = jsse_getD2FromL1D1L2(R, G, Lsx);
1331
+ let Dlsy = jsse_getD2FromL1D1L2(R, G, Lsy);
1332
+ Dix = Math.min(Dlsx, 1);
1333
+ Diy = Math.min(Dlsy, 1);
1334
+
1335
+ if (kValue > Jk1x) {
1336
+ Six = (kValue - Jk1x) / (2 - Jk1x);
1337
+ }
1338
+ if (kValue > Jk1y) {
1339
+ Siy = (kValue - Jk1y) / (2 - Jk1y);
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ Lx = Lix * (1 - Six);
1345
+ Ly = Liy * (1 - Siy);
1346
+ Sx = Lix * Six;
1347
+ Sy = Liy * Siy;
1348
+ Dx = Lx * Dix;
1349
+ Dy = Ly * Diy;
1350
+ }
1351
+
1352
+ let Qm = M;
1353
+ let Qw = width / M;
1354
+ let Qh = height / M;
1355
+ let Qr = r / M;
1356
+ let Q0 = { x: 0, y: 0 };
1357
+ let Q1 = { x: Qw, y: 0 };
1358
+ let Q2 = { x: Qw, y: Qh };
1359
+ let Q3 = { x: 0, y: Qh };
1360
+
1361
+ let pathCommands;
1362
+ if (kSign >= 0) {
1363
+ pathCommands = [
1364
+ `M`, Q0.x, Q0.y + (Sy + Ly) * Qr,
1365
+ `L`, Q0.x, Q0.y + (Ly) * Qr,
1366
+ `C`, Q0.x, Q0.y + (Ly - Dy) * Qr, Q0.x + (Lx - Dx) * Qr, Q0.y, Q0.x + (Lx) * Qr, Q0.y,
1367
+ `L`, Q0.x + (Sx + Lx) * Qr, Q0.y,
1368
+ `L`, Q1.x - (Sx + Lx) * Qr, Q1.y,
1369
+ `L`, Q1.x - (Lx) * Qr, Q1.y,
1370
+ `C`, Q1.x - (Lx - Dx) * Qr, Q1.y, Q1.x, Q1.y + (Ly - Dy) * Qr, Q1.x, Q1.y + (Ly) * Qr,
1371
+ `L`, Q1.x, Q1.y + (Sy + Ly) * Qr,
1372
+ `L`, Q2.x, Q2.y - (Sy + Ly) * Qr,
1373
+ `L`, Q2.x, Q2.y - (Ly) * Qr,
1374
+ `C`, Q2.x, Q2.y - (Ly - Dy) * Qr, Q2.x - (Lx - Dx) * Qr, Q2.y, Q2.x - (Lx) * Qr, Q2.y,
1375
+ `L`, Q2.x - (Sx + Lx) * Qr, Q2.y,
1376
+ `L`, Q3.x + (Sx + Lx) * Qr, Q3.y,
1377
+ `L`, Q3.x + (Lx) * Qr, Q3.y,
1378
+ `C`, Q3.x + (Lx - Dx) * Qr, Q3.y, Q3.x, Q3.y - (Ly - Dy) * Qr, Q3.x, Q3.y - (Ly) * Qr,
1379
+ `L`, Q3.x, Q3.y - (Sy + Ly) * Qr,
1380
+ 'Z'
1381
+ ];
1382
+ } else {
1383
+ pathCommands = [
1384
+ `M`, Q0.x, Q0.y + (Sy + Ly) * Qr,
1385
+ `L`, Q0.x + (Sx) * Qr, Q0.y + (Sy + Ly) * Qr,
1386
+ `C`, Q0.x + (Sx + Dx) * Qr, Q0.y + (Sy + Ly) * Qr, Q0.x + (Sx + Lx) * Qr, Q0.y + (Sy + Dy) * Qr, Q0.x + (Sx + Lx) * Qr, Q0.y + (Sy) * Qr,
1387
+ `L`, Q0.x + (Sx + Lx) * Qr, Q0.y,
1388
+ `L`, Q1.x - (Sx + Lx) * Qr, Q1.y,
1389
+ `L`, Q1.x - (Sx + Lx) * Qr, Q1.y + (Sy) * Qr,
1390
+ `C`, Q1.x - (Sx + Lx) * Qr, Q1.y + (Sy + Dy) * Qr, Q1.x - (Sx + Dx) * Qr, Q1.y + (Sy + Ly) * Qr, Q1.x - (Sx) * Qr, Q1.y + (Sy + Ly) * Qr,
1391
+ `L`, Q1.x, Q1.y + (Sy + Ly) * Qr,
1392
+ `L`, Q2.x, Q2.y - (Sy + Ly) * Qr,
1393
+ `L`, Q2.x - Sx * Qr, Q2.y - (Sy + Ly) * Qr,
1394
+ `C`, Q2.x - (Sx + Dx) * Qr, Q2.y - (Sy + Ly) * Qr, Q2.x - (Sx + Lx) * Qr, Q2.y - (Sy + Dy) * Qr, Q2.x - (Sx + Lx) * Qr, Q2.y - (Sy) * Qr,
1395
+ `L`, Q2.x - (Sx + Lx) * Qr, Q2.y,
1396
+ `L`, Q3.x + (Sx + Lx) * Qr, Q3.y,
1397
+ `L`, Q3.x + (Sx + Lx) * Qr, Q3.y - Sy * Qr,
1398
+ `C`, Q3.x + (Sx + Lx) * Qr, Q3.y - (Sy + Dy) * Qr, Q3.x + (Sx + Dx) * Qr, Q3.y - (Sy + Ly) * Qr, Q3.x + Sx * Qr, Q3.y - (Sy + Ly) * Qr,
1399
+ `L`, Q3.x, Q3.y - (Sy + Ly) * Qr,
1400
+ 'Z'
1401
+ ];
1402
+ }
1403
+
1404
+ /** Применяем масштабирование и округление **/
1405
+ const path = pathCommands.map(p => {
1406
+ if (typeof p === 'number') {
1407
+ return jsse_roundf(p * Qm, precision);
1408
+ }
1409
+ return p;
1410
+ }).join(' ');
1411
+
1412
+ return path;
1413
+ }
1414
+
1415
+ /**
1416
+ * @file src/mode.js
1417
+ *
1418
+ * @module sj-superellipse/mode
1419
+ * @since 1.0.0
1420
+ * @author f4n70m
1421
+ *
1422
+ * @description
1423
+ * Базовый класс `SuperellipseMode`, от которого наследуются конкретные реализации режимов.
1424
+ * Содержит общую логику захвата стилей и размеров элемента, пересчёта SVG-пути суперэллипса,
1425
+ * применения/восстановления CSS-свойств (`clip-path`). Определяет защищённые методы, которые должны
1426
+ * быть переопределены в дочерних классах.
1427
+ *
1428
+ * @example
1429
+ * class MyMode extends SuperellipseMode {
1430
+ * _getActivatedStyles() { return { /* стили активации *\}; }
1431
+ * _getModeName() { return 'my-mode'; }
1432
+ * _appendVirtualElements() { /* реализация *\/ }
1433
+ * _removeVirtualElements() { /* реализация *\/ }
1434
+ * }
1435
+ */
1436
+
1437
+
1438
+ /**
1439
+ * Базовый класс для реализации режимов суперэллипса.
1440
+ * @class SuperellipseMode
1441
+ * @since 1.0.0
1442
+ */
1443
+ class SuperellipseMode {
1444
+
1445
+ _element;
1446
+
1447
+ _isInitiated;
1448
+ _isActivated;
1449
+ // _isDebug;
1450
+
1451
+ _size = {
1452
+ width: 0,
1453
+ height: 0
1454
+ };
1455
+ _curveFactor;
1456
+ _precision;
1457
+
1458
+ _styles;
1459
+
1460
+ _path;
1461
+ _resetPath;
1462
+
1463
+
1464
+
1465
+
1466
+ /**
1467
+ * =============================================================
1468
+ * PUBLIC
1469
+ * =============================================================
1470
+ */
1471
+
1472
+
1473
+ /**
1474
+ * @since 1.0.0
1475
+ * @param {Element} element - Целевой элемент.
1476
+ * @param {boolean} [debug=false] - Флаг отладки.
1477
+ */
1478
+ constructor(element) {
1479
+
1480
+ this._element = element;
1481
+
1482
+ // this._isDebug = debug;
1483
+ this._isActivated = false;
1484
+
1485
+ this._curveFactor = jsse_getBorderRadiusFactor();
1486
+ this._precision = 2;
1487
+
1488
+ this._init();
1489
+ }
1490
+
1491
+ /**
1492
+ * Активирует режим.
1493
+ * @since 1.0.0
1494
+ * @returns {void}
1495
+ */
1496
+ activate() {
1497
+ if (this.isActivated()) return;
1498
+ jsse_console.debug({label:'MODE',element:this._element}, `activate(${this._getModeName()})`);
1499
+ /** Актуализировать данные захвата **/
1500
+ this._updateCaptured();
1501
+ /** Установить статус **/
1502
+ this._setStatus(true);
1503
+ /** Создать виртуальные элементы **/
1504
+ this._appendVirtualElements();
1505
+ /** Подготовить обновление **/
1506
+ this._prepareUpdate();
1507
+ /** Выполнить обновление **/
1508
+ this._executeUpdate();
1509
+ }
1510
+
1511
+ /**
1512
+ * Деактивирует режим.
1513
+ * @since 1.0.0
1514
+ * @returns {void}
1515
+ */
1516
+ deactivate() {
1517
+ if (!this.isActivated()) return;
1518
+ jsse_console.debug({label:'MODE',element:this._element}, `deactivate(${this._getModeName()})`);
1519
+ /** Установить статус **/
1520
+ this._setStatus(false);
1521
+ /** Удалить элементы виртуальных слоев **/
1522
+ this._removeVirtualElements();
1523
+ /** Выполнить обновление **/
1524
+ this._executeUpdate();
1525
+ }
1526
+
1527
+ /**
1528
+ * Полное обновление (стили, размер, путь).
1529
+ * @since 1.0.0
1530
+ * @returns {void}
1531
+ */
1532
+ update() {
1533
+ jsse_console.debug({label:'MODE',element:this._element}, 'update()');
1534
+ /** Актуализировать данные захвата **/
1535
+ this._updateCaptured();
1536
+ /** Подготовить обновление **/
1537
+ this._prepareUpdate();
1538
+ /** Выполнить обновление **/
1539
+ this._executeUpdate();
1540
+ }
1541
+
1542
+ /**
1543
+ * Обновление только размеров.
1544
+ * @since 1.0.0
1545
+ * @returns {void}
1546
+ */
1547
+ updateSize() {
1548
+ /** Актуализировать размеры **/
1549
+ this._updateCapturedSize();
1550
+ /** Подготовить обновление **/
1551
+ this._prepareUpdate();
1552
+ /** Выполнить обновление **/
1553
+ this._executeUpdate();
1554
+
1555
+ }
1556
+
1557
+ /**
1558
+ * Обновление только стилей.
1559
+ * @since 1.0.0
1560
+ * @returns {void}
1561
+ */
1562
+ updateStyles() {
1563
+ jsse_console.debug({label:'MODE',element:this._element}, 'updateStyles()');
1564
+ /** Актуализировать стили **/
1565
+ this._updateCapturedStyles();
1566
+ /** Подготовить обновление **/
1567
+ this._prepareUpdate();
1568
+ /** Выполнить обновление **/
1569
+ this._executeUpdate();
1570
+ }
1571
+
1572
+ /**
1573
+ * Обновление коэффициента кривизны.
1574
+ * @since 1.0.0
1575
+ * @param {number} value - Новое значение коэффициента кривизны.
1576
+ * @returns {void}
1577
+ */
1578
+ updateCurveFactor(value) {
1579
+ this.setCurveFactor(value);
1580
+ /** Подготовить обновление **/
1581
+ this._prepareUpdate();
1582
+ /** Выполнить обновление **/
1583
+ this._executeUpdate();
1584
+ }
1585
+
1586
+ /**
1587
+ * Обновление точности округления.
1588
+ * @since 1.0.0
1589
+ * @param {number} value - Количество знаков после запятой.
1590
+ * @returns {void}
1591
+ */
1592
+ updatePrecision(value) {
1593
+ this.setPrecision(value);
1594
+ /** Подготовить обновление **/
1595
+ this._prepareUpdate();
1596
+ /** Выполнить обновление **/
1597
+ this._executeUpdate();
1598
+ }
1599
+
1600
+ /**
1601
+ * Устанавливает коэффициент кривизны.
1602
+ * @since 1.0.0
1603
+ * @param {number} value - Новое значение коэффициента кривизны.
1604
+ * @returns {void}
1605
+ */
1606
+ setCurveFactor(value) {
1607
+ this._curveFactor = value;
1608
+ }
1609
+
1610
+ /**
1611
+ * Устанавливает точность округления.
1612
+ * @since 1.0.0
1613
+ * @param {number} value - Количество знаков после запятой.
1614
+ * @returns {void}
1615
+ */
1616
+ setPrecision(value) {
1617
+ this._precision = value;
1618
+ }
1619
+
1620
+ /**
1621
+ * Возвращает текущий SVG-путь.
1622
+ * @since 1.0.0
1623
+ * @returns {string}
1624
+ */
1625
+ getPath() {
1626
+ return this._path;
1627
+ }
1628
+
1629
+ /**
1630
+ * Проверяет, активирован ли режим.
1631
+ * @since 1.0.0
1632
+ * @returns {boolean}
1633
+ */
1634
+ isActivated() {
1635
+ return this._isActivated;
1636
+ }
1637
+
1638
+ /**
1639
+ * Уничтожает режим, удаляет все артефакты.
1640
+ * @since 1.0.0
1641
+ * @returns {void}
1642
+ */
1643
+ destroy() {
1644
+ this.deactivate();
1645
+ this._removeModeAttr();
1646
+ this._destroyResetStyles();
1647
+ }
1648
+
1649
+
1650
+ /**
1651
+ * =============================================================
1652
+ * PRIVATE
1653
+ * =============================================================
1654
+ */
1655
+
1656
+
1657
+ /**
1658
+ * Инициализация режима.
1659
+ * @since 1.0.0
1660
+ * @private
1661
+ * @returns {void}
1662
+ */
1663
+ _init() {
1664
+ this._id = Math.random().toString(36).slice(2, 10);
1665
+ this._initStyles();
1666
+ this._setModeAttr();
1667
+ this._initSize();
1668
+ this._initCurve();
1669
+ this._isInitiated = true;
1670
+ }
1671
+
1672
+ /**
1673
+ * Устанавливает статус активации.
1674
+ * @since 1.0.0
1675
+ * @private
1676
+ * @param {boolean} status - Статус активации.
1677
+ * @returns {void}
1678
+ */
1679
+ _setStatus(status) {
1680
+ this._isActivated = status;
1681
+ if (status) {
1682
+ this._setActivatedAttr();
1683
+ } else {
1684
+ this._removeActivatedAttr();
1685
+ }
1686
+ }
1687
+
1688
+ /**
1689
+ * Захватывает актуальные стили и размеры.
1690
+ * @since 1.0.0
1691
+ * @private
1692
+ * @returns {void}
1693
+ */
1694
+ _updateCaptured() {
1695
+ this._updateCapturedStyles();
1696
+ this._updateCapturedSize();
1697
+ }
1698
+
1699
+ /**
1700
+ * Подготавливает обновление (пересчёт кривой).
1701
+ * @since 1.0.0
1702
+ * @private
1703
+ * @returns {void}
1704
+ */
1705
+ _prepareUpdate() {
1706
+ this._recalculateCurve();
1707
+ }
1708
+
1709
+ /**
1710
+ * Выполняет обновление (применяет кривую).
1711
+ * @since 1.0.0
1712
+ * @private
1713
+ * @returns {void}
1714
+ */
1715
+ _executeUpdate() {
1716
+ this._applyCurrentCurve();
1717
+ }
1718
+
1719
+ /**
1720
+ * Возвращает имя режима.
1721
+ * @since 1.0.0
1722
+ * @protected
1723
+ * @returns {string}
1724
+ */
1725
+ _getModeName() {
1726
+ return 'clip-path';
1727
+ }
1728
+
1729
+ /**
1730
+ * Возвращает карту стилей, которые нужно временно применить для корректного чтения.
1731
+ * @since 1.0.0
1732
+ * @protected
1733
+ * @returns {Object<string, string>}
1734
+ */
1735
+ _getReadingStyles() {
1736
+ return {
1737
+ 'transition': 'unset'
1738
+ };
1739
+ }
1740
+
1741
+ /**
1742
+ * Возвращает карту стилей, применяемых при активации режима.
1743
+ * @since 1.0.0
1744
+ * @protected
1745
+ * @returns {Object<string, string>}
1746
+ */
1747
+ _getActivatedStyles() {
1748
+ return {
1749
+ 'border-radius': '0px'
1750
+ };
1751
+ }
1752
+
1753
+
1754
+ /**
1755
+ * =============================================================
1756
+ * VIRTUAL
1757
+ * =============================================================
1758
+ */
1759
+
1760
+
1761
+ /**
1762
+ * Создаёт виртуальные элементы (если нужно).
1763
+ * @since 1.0.0
1764
+ * @protected
1765
+ * @returns {void}
1766
+ */
1767
+ _appendVirtualElements() {}
1768
+
1769
+ /**
1770
+ * Удаляет виртуальные элементы.
1771
+ * @since 1.0.0
1772
+ * @protected
1773
+ * @returns {void}
1774
+ */
1775
+ _removeVirtualElements() {}
1776
+
1777
+
1778
+ /**
1779
+ * =============================================================
1780
+ * ATTRIBUTES
1781
+ * =============================================================
1782
+ */
1783
+
1784
+
1785
+ /**
1786
+ * Устанавливает атрибут `data-jsse-mode`.
1787
+ * @since 1.0.0
1788
+ * @protected
1789
+ * @returns {void}
1790
+ */
1791
+ _setModeAttr() {
1792
+ this._element.setAttribute('data-jsse-mode', this._getModeName());
1793
+ }
1794
+
1795
+ /**
1796
+ * Удаляет атрибут `data-jsse-mode`.
1797
+ * @since 1.0.0
1798
+ * @protected
1799
+ * @returns {void}
1800
+ */
1801
+ _removeModeAttr() {
1802
+ this._element.removeAttribute('data-jsse-mode');
1803
+ }
1804
+
1805
+ /**
1806
+ * Устанавливает атрибут `data-jsse-activated`.
1807
+ * @since 1.0.0
1808
+ * @protected
1809
+ * @returns {void}
1810
+ */
1811
+ _setActivatedAttr() {
1812
+ this._element.setAttribute('data-jsse-activated', true);
1813
+ }
1814
+
1815
+ /**
1816
+ * Удаляет атрибут `data-jsse-activated`.
1817
+ * @since 1.0.0
1818
+ * @protected
1819
+ * @returns {void}
1820
+ */
1821
+ _removeActivatedAttr() {
1822
+ this._element.removeAttribute('data-jsse-activated');
1823
+ }
1824
+
1825
+ /**
1826
+ * Устанавливает атрибут `data-jsse-reading`.
1827
+ * @since 1.0.0
1828
+ * @protected
1829
+ * @returns {void}
1830
+ */
1831
+ _setReadingAttr() {
1832
+ this._element.setAttribute('data-jsse-reading', true);
1833
+ }
1834
+
1835
+ /**
1836
+ * Удаляет атрибут `data-jsse-reading`.
1837
+ * @since 1.0.0
1838
+ * @protected
1839
+ * @returns {void}
1840
+ */
1841
+ _removeReadingAttr() {
1842
+ this._element.removeAttribute('data-jsse-reading');
1843
+ }
1844
+
1845
+
1846
+ /**
1847
+ * =============================================================
1848
+ * CSS
1849
+ * =============================================================
1850
+ */
1851
+
1852
+
1853
+ /**
1854
+ * Инициализирует глобальные CSS-правила для режима.
1855
+ * @since 1.0.0
1856
+ * @protected
1857
+ * @returns {void}
1858
+ */
1859
+ _initResetStyles() {
1860
+ const modeName = this._getModeName();
1861
+ if (!jsse_reset_css.has(modeName)) {
1862
+ const styleElement = this._createModeCssStyleElement(modeName);
1863
+ jsse_reset_css.set(modeName, styleElement);
1864
+ } else {
1865
+ jsse_reset_css.get(modeName).count++;
1866
+ }
1867
+
1868
+ jsse_console.debug({label:'MODE',element:this._element}, '[RESET STYLES]', 'INIT');
1869
+ // jsse_console.debug({label:'MODE'}, '[RESET STYLES]', '[INIT]', modeName, jsse_reset_css.get(modeName).count);
1870
+ }
1871
+
1872
+ /**
1873
+ * Уничтожает глобальные CSS-правила режима.
1874
+ * @since 1.0.0
1875
+ * @protected
1876
+ * @returns {void}
1877
+ */
1878
+ _destroyResetStyles() {
1879
+ const modeName = this._getModeName();
1880
+ if (!jsse_reset_css.has(modeName)) return;
1881
+
1882
+ const modeResetStyle = jsse_reset_css.get(modeName);
1883
+ modeResetStyle.count--;
1884
+
1885
+ jsse_console.debug({label:'MODE',element:this._element}, '[RESET STYLES]', '[DESTROY]');
1886
+ // jsse_console.debug({label:'MODE'}, '[RESET STYLES]', '[DESTROY]', modeName, jsse_reset_css.get(modeName).count);
1887
+
1888
+ if (modeResetStyle.count <= 0) {
1889
+ jsse_reset_css.unset(modeName);
1890
+ }
1891
+
1892
+ }
1893
+
1894
+ /**
1895
+ * Возвращает CSS-текст для сброса стилей режима.
1896
+ * @since 1.0.0
1897
+ * @protected
1898
+ * @param {string} modeName - Имя режима.
1899
+ * @returns {string}
1900
+ */
1901
+ _getResetCssText(modeName) {
1902
+ let cssString = '';
1903
+
1904
+ const activatedStyles = this._getActivatedStyles();
1905
+ cssString += `*:hover [data-jsse-mode="${modeName}"][data-jsse-activated=true],`;
1906
+ cssString += `[data-jsse-mode="${modeName}"][data-jsse-activated=true]:hover,`;
1907
+ cssString += `[data-jsse-mode="${modeName}"][data-jsse-activated=true]`;
1908
+ cssString += `{`;
1909
+ for (const prop in activatedStyles) {
1910
+ if (activatedStyles[prop] === '') continue;
1911
+ cssString += `\n\t${prop}: ${activatedStyles[prop]} !important;`;
1912
+ }
1913
+ cssString += `\n}`;
1914
+
1915
+ cssString += `\n`;
1916
+
1917
+ const readingStyles = this._getReadingStyles();
1918
+ cssString += `*:hover [data-jsse-mode="${modeName}"][data-jsse-reading=true],`;
1919
+ cssString += `[data-jsse-mode="${modeName}"][data-jsse-reading=true]:hover,`;
1920
+ cssString += `[data-jsse-mode="${modeName}"][data-jsse-reading=true]`;
1921
+ cssString += `{`;
1922
+ for (const prop in readingStyles) {
1923
+ if (readingStyles[prop] === '') continue;
1924
+ cssString += `\n\t${prop}: ${readingStyles[prop]} !important;`;
1925
+ }
1926
+ cssString += `\n}`;
1927
+
1928
+ return cssString;
1929
+ }
1930
+
1931
+ /**
1932
+ * Создаёт элемент `<style>` для режима.
1933
+ * @since 1.0.0
1934
+ * @protected
1935
+ * @param {string} modeName - Имя режима.
1936
+ * @returns {HTMLStyleElement}
1937
+ */
1938
+ _createModeCssStyleElement(modeName) {
1939
+ const textContent = this._getResetCssText(modeName);
1940
+ /** Создать элемент <style> **/
1941
+ const styleElement = document.createElement('style');
1942
+ styleElement.setAttribute('id', `jsse__css_${modeName}`);
1943
+ /** Заполнить элемент CSS-правилами **/
1944
+ styleElement.textContent = textContent;
1945
+ return styleElement;
1946
+ }
1947
+
1948
+
1949
+ /**
1950
+ * =============================================================
1951
+ * STYLES
1952
+ * =============================================================
1953
+ */
1954
+
1955
+
1956
+ /**
1957
+ * Обновляет захваченные стили.
1958
+ * @since 1.0.0
1959
+ * @protected
1960
+ * @returns {void}
1961
+ */
1962
+ _updateCapturedStyles() {
1963
+ jsse_console.debug({label:'MODE',element:this._element}, '_updateCapturedStyles()');
1964
+ const capturedComputedStyles = this._getCapturedStyles();
1965
+ /** Сохранить computed-стили **/
1966
+ this._styles.computed = capturedComputedStyles;
1967
+ }
1968
+
1969
+ /**
1970
+ * Получает вычисленные стили с временным снятием атрибута активации.
1971
+ * @since 1.0.0
1972
+ * @protected
1973
+ * @param {boolean} [clear=true] - Снимать ли атрибут активации перед чтением.
1974
+ * @returns {Object<string, string>}
1975
+ */
1976
+ _getCapturedStyles(clear = true) {
1977
+ const hasAttribute = this._element.hasAttribute('data-jsse-activated');
1978
+
1979
+ if (hasAttribute && clear) {
1980
+ this._removeActivatedAttr();
1981
+ }
1982
+ this._setReadingAttr();
1983
+
1984
+ const result = this._getManagedComputedStyle();
1985
+
1986
+ if (hasAttribute && clear) {
1987
+ this._setActivatedAttr();
1988
+ }
1989
+ this._removeReadingAttr();
1990
+ return result;
1991
+ }
1992
+
1993
+ /**
1994
+ * Получает вычисленные стили для управляемых свойств.
1995
+ * @since 1.0.0
1996
+ * @protected
1997
+ * @returns {Object<string, string>}
1998
+ */
1999
+ _getManagedComputedStyle() {
2000
+ const result = {};
2001
+ const capturedStyles = getComputedStyle(this._element);
2002
+ for (const prop of this._getManagedProperties()) {
2003
+ result[prop] = capturedStyles.getPropertyValue(prop);
2004
+ }
2005
+ return result;
2006
+ }
2007
+
2008
+ /**
2009
+ * Возвращает значение захваченного вычисленного свойства.
2010
+ * @since 1.0.0
2011
+ * @protected
2012
+ * @param {string} prop - Имя CSS-свойства.
2013
+ * @returns {string|undefined}
2014
+ */
2015
+ _getComputedProp(prop) {
2016
+ if ('computed' in this._styles && prop in this._styles.computed)
2017
+ return this._styles.computed[prop];
2018
+ }
2019
+
2020
+ /**
2021
+ * Возвращает массив свойств CSS, управляемых режимом.
2022
+ * @since 1.0.0
2023
+ * @protected
2024
+ * @returns {string[]}
2025
+ */
2026
+ _getManagedProperties() {
2027
+ return Object.keys(this._getActivatedStyles());
2028
+ }
2029
+
2030
+
2031
+ /**
2032
+ * =============================================================
2033
+ * CACHE
2034
+ * =============================================================
2035
+ */
2036
+
2037
+ /**
2038
+ * Инициализирует хранилище стилей.
2039
+ * @since 1.0.0
2040
+ * @protected
2041
+ * @returns {void}
2042
+ */
2043
+ _initStyles() {
2044
+ this._styles = jsse_styles.get(this._element);
2045
+ this._initResetStyles();
2046
+ }
2047
+
2048
+ /**
2049
+ * =============================================================
2050
+ * SIZE
2051
+ * =============================================================
2052
+ */
2053
+
2054
+
2055
+ /**
2056
+ * Инициализирует размеры.
2057
+ * @since 1.0.0
2058
+ * @protected
2059
+ * @returns {void}
2060
+ */
2061
+ _initSize() {
2062
+ this._updateCapturedSize();
2063
+ }
2064
+
2065
+ /**
2066
+ * Обновляет захваченные размеры.
2067
+ * @since 1.0.0
2068
+ * @protected
2069
+ * @returns {void}
2070
+ */
2071
+ _updateCapturedSize() {
2072
+ const rect = this._element.getBoundingClientRect();
2073
+
2074
+ this._size.width = rect.width;
2075
+ this._size.height = rect.height;
2076
+ }
2077
+
2078
+
2079
+ /**
2080
+ * =============================================================
2081
+ * CURVE
2082
+ * =============================================================
2083
+ */
2084
+
2085
+
2086
+ /**
2087
+ * Инициализирует кривую.
2088
+ * @since 1.0.0
2089
+ * @protected
2090
+ * @returns {void}
2091
+ */
2092
+ _initCurve() {
2093
+ this._initInlinePath();
2094
+ }
2095
+
2096
+ /**
2097
+ * Сохраняет исходный `clip-path`.
2098
+ * @since 1.0.0
2099
+ * @protected
2100
+ * @returns {void}
2101
+ */
2102
+ _initInlinePath() {
2103
+ this._resetPath = this._element.style.getPropertyValue('clip-path');
2104
+ }
2105
+
2106
+ /**
2107
+ * Пересчитывает путь на основе текущих размеров и радиуса.
2108
+ * @since 1.0.0
2109
+ * @protected
2110
+ * @returns {void}
2111
+ */
2112
+ _recalculateCurve() {
2113
+ this._recalculatePath();
2114
+ }
2115
+
2116
+ /**
2117
+ * Генерирует SVG-путь.
2118
+ * @since 1.0.0
2119
+ * @protected
2120
+ * @returns {void}
2121
+ */
2122
+ _recalculatePath() {
2123
+ if ( !( this._size.width > 0 && this._size.height > 0 ) ) {
2124
+ this._path = 'none'; // Сбрасываем путь
2125
+ return;
2126
+ }
2127
+
2128
+ const radiusValue = this._getComputedProp('border-radius');
2129
+ const radiusNumber = radiusValue ? parseFloat(radiusValue) : 0;
2130
+
2131
+ this._path = jsse_generateSuperellipsePath(
2132
+ this._size.width,
2133
+ this._size.height,
2134
+ radiusNumber,
2135
+ this._curveFactor,
2136
+ this._precision
2137
+ );
2138
+ }
2139
+
2140
+ /**
2141
+ * Применяет текущий путь (если активирован) или восстанавливает исходный.
2142
+ * @since 1.0.0
2143
+ * @protected
2144
+ * @returns {void}
2145
+ */
2146
+ _applyCurrentCurve() {
2147
+ if (this.isActivated()) {
2148
+ this._applyCurve();
2149
+ } else {
2150
+ this._restoreCurve();
2151
+ }
2152
+ }
2153
+
2154
+ /**
2155
+ * Применяет суперэллипс через `clip-path`.
2156
+ * @since 1.0.0
2157
+ * @protected
2158
+ * @returns {void}
2159
+ */
2160
+ _applyCurve() {
2161
+ if (this._path && this._path !== 'none') {
2162
+ const newPath = `path("${this._path}")`;
2163
+ if (this._element.style.getPropertyValue('clip-path') !== newPath) {
2164
+ this._element.style.setProperty('clip-path', newPath);
2165
+ }
2166
+ } else {
2167
+ if (this._element.style.getPropertyValue('clip-path') !== 'none') {
2168
+ this._element.style.setProperty('clip-path', 'none');
2169
+ }
2170
+ }
2171
+ }
2172
+
2173
+ /**
2174
+ * Восстанавливает исходный `clip-path`.
2175
+ * @since 1.0.0
2176
+ * @protected
2177
+ * @returns {void}
2178
+ */
2179
+ _restoreCurve() {
2180
+ if (this._resetPath) {
2181
+ this._element.style.setProperty('clip-path', this._resetPath);
2182
+ } else {
2183
+ this._element.style.removeProperty('clip-path');
2184
+ }
2185
+ }
2186
+
2187
+
2188
+ /**
2189
+ * =============================================================
2190
+ *
2191
+ * =============================================================
2192
+ */
2193
+ }
2194
+
2195
+ /**
2196
+ * @file src/mode-svg-layer.js
2197
+ *
2198
+ * @module sj-superellipse/mode-svg-layer
2199
+ * @since 1.0.0
2200
+ * @author f4n70m
2201
+ *
2202
+ * @description
2203
+ * Режим `svg-layer` – полнофункциональный режим, создающий наложенный SVG-слой для отрисовки
2204
+ * суперэллипса. Позволяет корректно отображать `background`, `border`, `box-shadow` элемента.
2205
+ * Переносит дочернее содержимое во внутренний div-контейнер, а поверх него размещает SVG,
2206
+ * который повторяет геометрию суперэллипса с возможностью применения градиентов, теней и
2207
+ * произвольных стилей обводки.
2208
+ *
2209
+ * @extends SuperellipseMode
2210
+ * @example
2211
+ * const mode = new SuperellipseModeSvgLayer(element);
2212
+ * mode.activate();
2213
+ */
2214
+
2215
+
2216
+
2217
+ /**
2218
+ * Режим, создающий наложенный SVG-слой для отрисовки фона, границ и теней.
2219
+ * @class SuperellipseModeSvgLayer
2220
+ * @extends SuperellipseMode
2221
+ */
2222
+ class SuperellipseModeSvgLayer extends SuperellipseMode {
2223
+
2224
+ /**
2225
+ * Хранилище ссылок на созданные виртуальные DOM-элементы (svg, div, path и т.д.).
2226
+ * @since 1.0.0
2227
+ * @type {Object<string, Element>}
2228
+ * @protected
2229
+ */
2230
+ _virtualElementList = {};
2231
+
2232
+ /**
2233
+ * Текущая строка viewBox для SVG.
2234
+ * @since 1.0.0
2235
+ * @type {string}
2236
+ * @protected
2237
+ */
2238
+ _viewbox;
2239
+
2240
+
2241
+ /**
2242
+ * =============================================================
2243
+ * PUBLIC
2244
+ * =============================================================
2245
+ */
2246
+
2247
+
2248
+ /**
2249
+ * Создаёт экземпляр режима svg-layer.
2250
+ * @since 1.0.0
2251
+ * @param {Element} element - Целевой DOM-элемент.
2252
+ * @param {boolean} [debug=false] - Флаг отладки.
2253
+ */
2254
+ constructor(element, debug = false) {
2255
+ super(element, debug);
2256
+
2257
+ this._initViewbox();
2258
+ this._initVirtualElementList();
2259
+ }
2260
+
2261
+
2262
+ /**
2263
+ * =============================================================
2264
+ * PRIVATE
2265
+ * =============================================================
2266
+ */
2267
+
2268
+
2269
+ /**
2270
+ * Выполняет обновление: применяет стили к слою div и обновляет путь.
2271
+ * @since 1.0.0
2272
+ * @override
2273
+ * @protected
2274
+ * @returns {void}
2275
+ */
2276
+ _executeUpdate() {
2277
+ this._applyCurrentInlineVirtualSvgLayerStyles();
2278
+ this._applyCurrentCurve();
2279
+ }
2280
+
2281
+ /**
2282
+ * Возвращает имя режима ('svg-layer').
2283
+ * @since 1.0.0
2284
+ * @override
2285
+ * @protected
2286
+ * @returns {string}
2287
+ */
2288
+ _getModeName() {
2289
+ return 'svg-layer';
2290
+ }
2291
+
2292
+ /**
2293
+ * Возвращает стили, применяемые к основному элементу при активации.
2294
+ * @since 1.0.0
2295
+ * @override
2296
+ * @protected
2297
+ * @returns {Object<string, string>}
2298
+ */
2299
+ _getActivatedStyles() {
2300
+ return {
2301
+ 'background': 'none',
2302
+ 'border-color': 'transparent',
2303
+ 'border-width': '',
2304
+ 'border-width': '0px',
2305
+ // 'border-style': 'none',
2306
+ // 'border': 'unset',
2307
+ 'border-radius': '0px',
2308
+ 'box-shadow': 'unset',
2309
+ 'position': 'relative',
2310
+ };
2311
+ }
2312
+
2313
+ /**
2314
+ * Возвращает стили для SVG-контейнера.
2315
+ * @since 1.0.0
2316
+ * @protected
2317
+ * @returns {Object<string, string>}
2318
+ */
2319
+ _getSvgLayerStyles() {
2320
+ return {
2321
+ 'position': 'absolute',
2322
+ 'top': '0px',
2323
+ 'left': '0px',
2324
+ 'width': '100%',
2325
+ 'height': '100%',
2326
+ 'pointer-events': 'none'
2327
+ };
2328
+ }
2329
+
2330
+ /**
2331
+ * Возвращает список CSS-свойств, которые переносятся во внутренний div.
2332
+ * @since 1.0.0
2333
+ * @protected
2334
+ * @returns {string[]}
2335
+ */
2336
+ _getSvgLayerDivProps() {
2337
+ return [
2338
+ // 'color',
2339
+ 'background',
2340
+ // 'background-size',
2341
+ // 'background-position',
2342
+ 'box-shadow'
2343
+ ];
2344
+ }
2345
+
2346
+
2347
+ /**
2348
+ * =============================================================
2349
+ * STYLES
2350
+ * =============================================================
2351
+ */
2352
+
2353
+
2354
+ /**
2355
+ * Применяет инлайновые стили к указанному элементу.
2356
+ * @since 1.0.0
2357
+ * @protected
2358
+ * @param {Object<string, string>} props - Объект стилей.
2359
+ * @param {HTMLElement|SVGElement} element - Целевой элемент.
2360
+ * @returns {void}
2361
+ */
2362
+ _applyInlineStyles(props, element) {
2363
+ const managedProperties = this._getManagedProperties();
2364
+ for(const prop of managedProperties) {
2365
+ const inlineValue = props[prop];
2366
+ const currentValue = element.style.getPropertyValue(prop);
2367
+ if (inlineValue !== undefined) {
2368
+ if (currentValue !== inlineValue) {
2369
+ element.style.setProperty(prop, inlineValue);
2370
+ }
2371
+ } else {
2372
+ if (currentValue !== '') {
2373
+ element.style.removeProperty(prop);
2374
+ }
2375
+ }
2376
+ }
2377
+ }
2378
+
2379
+ /**
2380
+ * Применяет все стили (div, border, shadows) к виртуальным слоям, если режим активен.
2381
+ * @since 1.0.0
2382
+ * @protected
2383
+ * @returns {void}
2384
+ */
2385
+ _applyCurrentInlineVirtualSvgLayerStyles() {
2386
+ if ( this.isActivated() ) {
2387
+ this._applyCurrentInlineVirtualSvgLayerDivStyles();
2388
+ this._applyCurrentInlineVirtualSvgLayerBorderStyles();
2389
+ this._applyCurrentInlineVirtualSvgLayerShadowsStyles();
2390
+ }
2391
+ }
2392
+
2393
+
2394
+ /**
2395
+ * Применяет стили к виртуальному div-слою.
2396
+ * @since 1.0.0
2397
+ * @protected
2398
+ * @returns {void}
2399
+ */
2400
+ _applyCurrentInlineVirtualSvgLayerDivStyles() {
2401
+ const inlineSvgLayerDivStyles = this._getCurrentInlineVirtualSvgLayerDivStyles();
2402
+ const svgLayerDiv = this._virtualElementList.svgLayerDiv;
2403
+ this._applyInlineStyles(inlineSvgLayerDivStyles, svgLayerDiv);
2404
+ }
2405
+
2406
+ /**
2407
+ * Возвращает стили для внутреннего div-слоя, извлечённые из вычисленных стилей элемента.
2408
+ * @since 1.0.0
2409
+ * @protected
2410
+ * @returns {Object<string, string>}
2411
+ */
2412
+ _getCurrentInlineVirtualSvgLayerDivStyles() {
2413
+ // jsse_console.debug(this._element, '[MODE SvgLayer]', '_getCurrentInlineVirtualSvgLayerDivStyles()');
2414
+ const result = {};
2415
+ const svgLayerDivProps = this._getSvgLayerDivProps();
2416
+ for (const prop of svgLayerDivProps) {
2417
+ const value = this._getComputedProp(prop);
2418
+ if (value !== undefined) {
2419
+ result[prop] = value;
2420
+ }
2421
+ }
2422
+ return result;
2423
+ }
2424
+
2425
+ /**
2426
+ * Применяет стили границы (цвет, толщину, стиль) к SVG-элементу `border`.
2427
+ * @since 1.0.0
2428
+ * @protected
2429
+ * @returns {void}
2430
+ */
2431
+ _applyCurrentInlineVirtualSvgLayerBorderStyles() {
2432
+ const svgLayerBorder = this._virtualElementList.svgLayerBorder;
2433
+ const borderColor = this._getComputedProp('border-color');
2434
+ svgLayerBorder.setAttribute('stroke', borderColor);
2435
+ const borderWidth = this._getComputedProp('border-width');
2436
+ const borderWidthNumber = borderWidth ? (parseFloat(borderWidth) * 2) : 0;
2437
+ svgLayerBorder.setAttribute('stroke-width', borderWidthNumber);
2438
+ const borderStyle = this._getComputedProp('border-style');
2439
+ this._applyBorderStyleToStroke(borderStyle, svgLayerBorder);
2440
+ }
2441
+
2442
+ /**
2443
+ * Устанавливает атрибут `stroke-dasharray` для элемента пути.
2444
+ * @since 1.0.0
2445
+ * @protected
2446
+ * @param {SVGElement} pathElement - SVG-элемент (обычно path или use).
2447
+ * @param {string} value - Значение dasharray.
2448
+ * @returns {void}
2449
+ */
2450
+ _setStrokeDasharray(pathElement, value) {
2451
+ pathElement.setAttribute('stroke-dasharray', value);
2452
+ }
2453
+
2454
+ /**
2455
+ * Устанавливает атрибут `stroke-linecap` для элемента пути.
2456
+ * @since 1.0.0
2457
+ * @protected
2458
+ * @param {SVGElement} pathElement - SVG-элемент.
2459
+ * @param {string} value - Значение linecap (butt, round, square).
2460
+ * @returns {void}
2461
+ */
2462
+ _setStrokeLinecap(pathElement, value) {
2463
+ pathElement.setAttribute('stroke-linecap', value);
2464
+ }
2465
+
2466
+ /**
2467
+ * Устанавливает атрибут `stroke-opacity` для элемента пути.
2468
+ * @since 1.0.0
2469
+ * @protected
2470
+ * @param {SVGElement} pathElement - SVG-элемент.
2471
+ * @param {string|number} value - Прозрачность (0..1).
2472
+ * @returns {void}
2473
+ */
2474
+ _setStrokeOpacity(pathElement, value) {
2475
+ pathElement.setAttribute('stroke-opacity', value);
2476
+ }
2477
+
2478
+ /**
2479
+ * Преобразует CSS-стиль границы в атрибуты SVG-элемента.
2480
+ * @since 1.0.0
2481
+ * @protected
2482
+ * @param {string} borderStyle - Стиль границы (solid, dotted, dashed и т.д.).
2483
+ * @param {SVGElement} pathElement - Элемент, к которому применяется обводка.
2484
+ * @returns {void}
2485
+ */
2486
+ _applyBorderStyleToStroke(borderStyle, pathElement) {
2487
+ /** Сброс атрибутов **/
2488
+ pathElement.removeAttribute('stroke-dasharray');
2489
+ pathElement.removeAttribute('stroke-linecap');
2490
+ pathElement.removeAttribute('stroke-linejoin');
2491
+
2492
+ switch(borderStyle) {
2493
+ case 'solid':
2494
+ /** Сплошная линия (значения по умолчанию) **/
2495
+ break;
2496
+
2497
+ case 'dotted':
2498
+ this._setStrokeDasharray(pathElement, '0, 8');
2499
+ this._setStrokeLinecap(pathElement, 'round');
2500
+ break;
2501
+
2502
+ case 'dashed':
2503
+ this._setStrokeDasharray(pathElement, '10, 6');
2504
+ break;
2505
+
2506
+ case 'double':
2507
+ /** Для double нужно два отдельных пути или фильтр **/
2508
+ jsse_console.warn({label:'MODE SVG LAYER',element:this._element}, '«border-style: double» is not supported');
2509
+ break;
2510
+
2511
+ case 'groove':
2512
+ this._setStrokeDasharray(pathElement, '1, 2');
2513
+ this._setStrokeLinecap(pathElement, 'round');
2514
+ break;
2515
+
2516
+ case 'ridge':
2517
+ this._setStrokeDasharray(pathElement, '3, 3');
2518
+ break;
2519
+
2520
+ case 'inset':
2521
+ /** Имитация inset через полупрозрачность **/
2522
+ this._setStrokeOpacity(pathElement, '0.7');
2523
+ break;
2524
+
2525
+ case 'outset':
2526
+ this._setStrokeOpacity(pathElement, '0.5');
2527
+ break;
2528
+
2529
+ case 'dash-dot':
2530
+ /** Кастомный стиль **/
2531
+ this._setStrokeDasharray(pathElement, '15, 5, 5, 5');
2532
+ break;
2533
+
2534
+ case 'dash-dot-dot':
2535
+ /** Кастомный стиль **/
2536
+ this._setStrokeDasharray(pathElement, '15, 5, 5, 5, 5, 5');
2537
+ break;
2538
+ }
2539
+ }
2540
+
2541
+ /**
2542
+ * Применяет тени (`box-shadow`) к SVG-слою, создавая фильтры.
2543
+ * @since 1.0.0
2544
+ * @protected
2545
+ * @returns {void}
2546
+ */
2547
+ _applyCurrentInlineVirtualSvgLayerShadowsStyles() {
2548
+ const boxShadowValue = this._getComputedProp('box-shadow');
2549
+ const shadows = this._parseBoxShadow(boxShadowValue);
2550
+
2551
+ const svg = this._virtualElementList.svgLayer;
2552
+ const gFilters = this._virtualElementList.svgLayerGFilters;
2553
+ const gShadows = this._virtualElementList.svgLayerGShadows;
2554
+ const path = this._virtualElementList.svgLayerPath;
2555
+
2556
+ gFilters.replaceChildren();
2557
+ gShadows.replaceChildren();
2558
+
2559
+ const id = svg.getAttribute('id');
2560
+ const pathId = path.getAttribute('id');
2561
+
2562
+ for (let i = 0; i < shadows.length; i++) {
2563
+
2564
+ if (shadows[i].inset) continue;
2565
+
2566
+ const shadowValues = shadows[i];
2567
+ shadowValues.spreadRadius;
2568
+
2569
+ const filter = this._createVirtualSvgElement('filter');
2570
+ const feGaussianBlur = this._createVirtualSvgElement('feGaussianBlur');
2571
+ const feOffset = this._createVirtualSvgElement('feOffset');
2572
+ const feFlood = this._createVirtualSvgElement('feFlood');
2573
+ const feComposite = this._createVirtualSvgElement('feComposite');
2574
+ const shadow = this._createVirtualSvgElement('use');
2575
+
2576
+ const filterId = `${id}__filter_${i}`;
2577
+ const filterBlurId = `${filterId}__blur`;
2578
+ const filterOffsetId = `${filterId}__offset`;
2579
+ const filterColorId = `${filterId}__color`;
2580
+ const filterShadowId = `${filterId}__shadow`;
2581
+ const shadowId = `${id}__shadow_${i}`;
2582
+
2583
+ gFilters.appendChild(filter);
2584
+ filter.setAttribute('id', filterId);
2585
+ filter.setAttribute('x', '-100%');
2586
+ filter.setAttribute('y', '-100%');
2587
+ filter.setAttribute('width', '300%');
2588
+ filter.setAttribute('height', '300%');
2589
+ filter.appendChild(feGaussianBlur);
2590
+ filter.appendChild(feOffset);
2591
+ filter.appendChild(feFlood);
2592
+ filter.appendChild(feComposite);
2593
+ feGaussianBlur.setAttribute('in', 'SourceAlpha');
2594
+ feGaussianBlur.setAttribute('stdDeviation', shadowValues.blurRadius / 2);
2595
+ feGaussianBlur.setAttribute('result', filterBlurId);
2596
+ feOffset.setAttribute('dx', shadowValues.offsetX);
2597
+ feOffset.setAttribute('dy', shadowValues.offsetY);
2598
+ feOffset.setAttribute('in', filterBlurId);
2599
+ feOffset.setAttribute('result', filterOffsetId);
2600
+ feFlood.style.setProperty('flood-color', shadowValues.color);
2601
+ feFlood.setAttribute('result', filterColorId);
2602
+ feComposite.setAttribute('in', filterColorId);
2603
+ feComposite.setAttribute('in2', filterOffsetId);
2604
+ feComposite.setAttribute('operator', 'in');
2605
+ feComposite.setAttribute('result', filterShadowId);
2606
+
2607
+ gShadows.appendChild(shadow);
2608
+ shadow.setAttribute('href', `#${pathId}`);
2609
+ shadow.setAttribute('id', shadowId);
2610
+ shadow.setAttribute('filter', `url(#${filterId})`);
2611
+ }
2612
+ }
2613
+
2614
+ /**
2615
+ * Разбирает значение `box-shadow` на массив объектов теней.
2616
+ * @since 1.0.0
2617
+ * @protected
2618
+ * @param {string} boxShadowValue - Строка свойства `box-shadow`.
2619
+ * @returns {Array<Object>} Массив теней с полями: inset, color, offsetX, offsetY, blurRadius, spreadRadius, originalColorFormat.
2620
+ */
2621
+ _parseBoxShadow(boxShadowValue) {
2622
+ if (!boxShadowValue || boxShadowValue === 'none') return [];
2623
+
2624
+ /** Разделяем тени **/
2625
+ const shadows = [];
2626
+ let current = '';
2627
+ let depth = 0;
2628
+
2629
+ for (let char of boxShadowValue) {
2630
+ if (char === '(') depth++;
2631
+ if (char === ')') depth--;
2632
+ if (char === ',' && depth === 0) {
2633
+ shadows.push(current.trim());
2634
+ current = '';
2635
+ } else {
2636
+ current += char;
2637
+ }
2638
+ }
2639
+ shadows.push(current.trim());
2640
+
2641
+ return shadows.map(shadow => {
2642
+ /** Парсим одну тень **/
2643
+ const parts = shadow.match(/(?:rgba?\([^)]+\)|\S+)/g);
2644
+ if (!parts) return null;
2645
+
2646
+ const result = {
2647
+ inset: false,
2648
+ color: null,
2649
+ offsetX: 0,
2650
+ offsetY: 0,
2651
+ blurRadius: 0,
2652
+ spreadRadius: 0,
2653
+ originalColorFormat: null // исходный формат (уже нормализован getComputedStyle)
2654
+ };
2655
+
2656
+ /** Проверяем inset **/
2657
+ const insetIndex = parts.indexOf('inset');
2658
+ if (insetIndex !== -1) {
2659
+ result.inset = true;
2660
+ parts.splice(insetIndex, 1);
2661
+ }
2662
+
2663
+ /** Цвет всегда будет в формате rgb/rgba после getComputedStyle **/
2664
+ const colorPart = parts.find(p => p.startsWith('rgb'));
2665
+ if (colorPart) {
2666
+ result.color = colorPart;
2667
+ result.originalColorFormat = colorPart;
2668
+ parts.splice(parts.indexOf(colorPart), 1);
2669
+ }
2670
+
2671
+ /** Парсим числовые значения **/
2672
+ const numbers = parts
2673
+ .map(p => parseFloat(p))
2674
+ .filter(n => !isNaN(n));
2675
+
2676
+ if (numbers[0] !== undefined) result.offsetX = numbers[0];
2677
+ if (numbers[1] !== undefined) result.offsetY = numbers[1];
2678
+ if (numbers[2] !== undefined) result.blurRadius = numbers[2];
2679
+ if (numbers[3] !== undefined) result.spreadRadius = numbers[3];
2680
+
2681
+ return result;
2682
+ });
2683
+ }
2684
+
2685
+
2686
+ /**
2687
+ * =============================================================
2688
+ * VIRTUAL
2689
+ * =============================================================
2690
+ */
2691
+
2692
+
2693
+ /**
2694
+ * Инициализирует список виртуальных элементов (div, svg и пр.).
2695
+ * @since 1.0.0
2696
+ * @protected
2697
+ * @returns {void}
2698
+ */
2699
+ _initVirtualElementList() {
2700
+ this._initVirtualInnerWrapper();
2701
+ this._initVirtualSvgLayer();
2702
+ }
2703
+
2704
+ /**
2705
+ * Создаёт SVG-элемент с указанным тегом в пространстве имён SVG.
2706
+ * @since 1.0.0
2707
+ * @protected
2708
+ * @param {string} tag - Имя тега (например, 'svg', 'path', 'filter').
2709
+ * @returns {SVGElement}
2710
+ */
2711
+ _createVirtualSvgElement(tag) {
2712
+ return document.createElementNS('http://www.w3.org/2000/svg', tag);
2713
+ }
2714
+
2715
+ /**
2716
+ * Создаёт HTML-элемент с указанным тегом.
2717
+ * @since 1.0.0
2718
+ * @protected
2719
+ * @param {string} tag - Имя тега (например, 'div').
2720
+ * @returns {HTMLElement}
2721
+ */
2722
+ _createVirtualHtmlElement(tag) {
2723
+ return document.createElement(tag);
2724
+ }
2725
+
2726
+ /**
2727
+ * Создаёт SVG-элементы для слоя.
2728
+ * @since 1.0.0
2729
+ * @protected
2730
+ * @returns {void}
2731
+ */
2732
+ _initVirtualSvgLayer() {
2733
+ if (this._virtualElementList.svgLayer) return;
2734
+
2735
+ const id = this._id;
2736
+ const svgId = `jsse_${id}`;
2737
+ const clipId = `jsse_${id}__clip`;
2738
+ const pathId = `jsse_${id}__path`;
2739
+ const filtersId = `jsse_${id}__filters`;
2740
+ const shadowsId = `jsse_${id}__shadows`;
2741
+ const divId = `jsse_${id}__div`;
2742
+ const borderId = `jsse_${id}__border`;
2743
+
2744
+ const svg = this._createVirtualSvgElement('svg');
2745
+ const defs = this._createVirtualSvgElement('defs');
2746
+ const clipPath = this._createVirtualSvgElement('clipPath');
2747
+ const path = this._createVirtualSvgElement('path');
2748
+ const gFilters = this._createVirtualSvgElement('g');
2749
+ const gShadows = this._createVirtualSvgElement('g');
2750
+ const html = this._createVirtualSvgElement('foreignObject');
2751
+ const div = this._createVirtualHtmlElement('div');
2752
+ const border = this._createVirtualSvgElement('use');
2753
+
2754
+ /** svg **/
2755
+ svg.setAttribute('id', svgId);
2756
+ svg.classList.add('jsse--svg-layer--bg');
2757
+ svg.setAttribute('viewBox', this._getViewbox());
2758
+ svg.setAttribute('preserveAspectRatio', 'none');
2759
+ const svgProps = this._getSvgLayerStyles();
2760
+ for (const prop in svgProps) {
2761
+ svg.style.setProperty(prop, svgProps[prop]);
2762
+ }
2763
+ svg.style.setProperty('overflow', 'visible');
2764
+ svg.appendChild(defs);
2765
+ svg.appendChild(gShadows);
2766
+ svg.appendChild(html);
2767
+ svg.appendChild(border);
2768
+ /** svg > defs **/
2769
+ defs.appendChild(clipPath);
2770
+ defs.appendChild(gFilters);
2771
+ /** svg > defs > clipPath **/
2772
+ clipPath.setAttribute('id', clipId);
2773
+ clipPath.appendChild(path);
2774
+ /** svg > defs > clipPath > path **/
2775
+ path.setAttribute('id', pathId);
2776
+ path.setAttribute('d', ''); // только создаем слой, не заполняем
2777
+ /** svg > defs > g **/
2778
+ gFilters.setAttribute('id', filtersId);
2779
+ /** svg > g **/
2780
+ gShadows.setAttribute('id', shadowsId);
2781
+ /** svg > foreignObject **/
2782
+ html.setAttribute('id', shadowsId);
2783
+ html.setAttribute('width', '100%');
2784
+ html.setAttribute('height', '100%');
2785
+ html.setAttribute('clip-path', `url(#${clipId})`);
2786
+ html.appendChild(div);
2787
+ /** svg > foreignObject > div **/
2788
+ div.setAttribute('id', divId);
2789
+ div.style.setProperty('width', '100%');
2790
+ div.style.setProperty('height', '100%');
2791
+ /** svg > border **/
2792
+ border.setAttribute('id', borderId);
2793
+ border.setAttribute('href', `#${pathId}`);
2794
+ border.setAttribute('fill', 'none');
2795
+ border.setAttribute('stroke', '');
2796
+ border.setAttribute('stroke-width', '0');
2797
+ border.setAttribute('clip-path', `url(#${clipId})`);
2798
+
2799
+
2800
+ this._virtualElementList.svgLayer = svg;
2801
+ this._virtualElementList.svgLayerPath = path;
2802
+ this._virtualElementList.svgLayerGFilters = gFilters;
2803
+ this._virtualElementList.svgLayerGShadows = gShadows;
2804
+ this._virtualElementList.svgLayerDiv = div;
2805
+ this._virtualElementList.svgLayerBorder = border;
2806
+ }
2807
+
2808
+ /**
2809
+ * Создаёт внутренний div-обёртку для контента.
2810
+ * @since 1.0.0
2811
+ * @protected
2812
+ * @returns {void}
2813
+ */
2814
+ _initVirtualInnerWrapper() {
2815
+ if (this._virtualElementList.innerWrapper) return;
2816
+
2817
+ const innerWrapper = this._createVirtualHtmlElement('div');
2818
+ innerWrapper.className = 'jsse--svg-layer--content';
2819
+ innerWrapper.style.setProperty('position', 'relative');
2820
+
2821
+ this._virtualElementList.innerWrapper = innerWrapper;
2822
+ }
2823
+
2824
+ /**
2825
+ * Добавляет виртуальные элементы в DOM.
2826
+ * @override
2827
+ * @since 1.0.0
2828
+ * @protected
2829
+ * @returns {void}
2830
+ */
2831
+ _appendVirtualElements() {
2832
+ this._appendInnerWrapper();
2833
+ this._appendSvgLayer();
2834
+ }
2835
+
2836
+ /**
2837
+ * Удаляет виртуальные элементы из DOM.
2838
+ * @override
2839
+ * @since 1.0.0
2840
+ * @protected
2841
+ * @returns {void}
2842
+ */
2843
+ _removeVirtualElements() {
2844
+ this._removeSvgLayer();
2845
+ this._removeInnerWrapper();
2846
+ }
2847
+
2848
+ /**
2849
+ * Добавляет SVG-слой в начало элемента.
2850
+ * @since 1.0.0
2851
+ * @protected
2852
+ * @returns {void}
2853
+ */
2854
+ _appendSvgLayer() {
2855
+ const svgLayer = this._virtualElementList.svgLayer;
2856
+ // TODO: нужна ли проверка на наличие svgLayer у this._element?
2857
+ this._element.insertBefore(svgLayer, this._element.firstChild);
2858
+ }
2859
+
2860
+ /**
2861
+ * Удаляет SVG-слой.
2862
+ * @since 1.0.0
2863
+ * @protected
2864
+ * @returns {void}
2865
+ */
2866
+ _removeSvgLayer() {
2867
+ const svgLayer = this._virtualElementList.svgLayer;
2868
+ if (svgLayer && svgLayer.parentNode === this._element) {
2869
+ this._element.removeChild(svgLayer);
2870
+ }
2871
+ }
2872
+
2873
+ /**
2874
+ * Перемещает дочерние элементы элемента во внутреннюю обёртку.
2875
+ * @since 1.0.0
2876
+ * @protected
2877
+ * @returns {void}
2878
+ */
2879
+ _appendInnerWrapper() {
2880
+ const innerWrapper = this._virtualElementList.innerWrapper;
2881
+
2882
+ /** Перемещаем внутренние элемены this._element в innerWrapper **/
2883
+ const children = Array.from(this._element.childNodes);
2884
+ for (const child of children) {
2885
+ innerWrapper.appendChild(child);
2886
+ }
2887
+
2888
+ this._element.appendChild(innerWrapper);
2889
+ }
2890
+
2891
+ /**
2892
+ * Возвращает дочерние элементы из обёртки обратно в элемент.
2893
+ * @since 1.0.0
2894
+ * @protected
2895
+ * @returns {void}
2896
+ */
2897
+ _removeInnerWrapper() {
2898
+ const innerWrapper = this._virtualElementList.innerWrapper;
2899
+
2900
+ /** Перемещаем внутренние элемены innerWrapper в this._element **/
2901
+ const children = Array.from(innerWrapper.childNodes);
2902
+ for (const child of children) {
2903
+ this._element.appendChild(child);
2904
+ }
2905
+
2906
+ this._element.removeChild(innerWrapper);
2907
+ }
2908
+
2909
+
2910
+
2911
+ /**
2912
+ * =============================================================
2913
+ * CURVE
2914
+ * =============================================================
2915
+ */
2916
+
2917
+
2918
+ /**
2919
+ * Инициализирует viewBox.
2920
+ * @since 1.0.0
2921
+ * @protected
2922
+ * @returns {void}
2923
+ */
2924
+ _initViewbox() {
2925
+ this._recalculateViewbox();
2926
+ }
2927
+
2928
+ /**
2929
+ * Пересчитывает viewBox при изменении размеров.
2930
+ * @override
2931
+ * @since 1.0.0
2932
+ * @protected
2933
+ * @returns {void}
2934
+ */
2935
+ _recalculateCurve() {
2936
+ super._recalculateCurve();
2937
+
2938
+ this._recalculateViewbox();
2939
+ }
2940
+
2941
+ /**
2942
+ * Обновляет viewBox на основе текущих размеров.
2943
+ * @since 1.0.0
2944
+ * @protected
2945
+ * @returns {void}
2946
+ */
2947
+ _recalculateViewbox() {
2948
+ if ( this._size.width > 0 && this._size.height > 0 ) {
2949
+ this._viewbox = `0 0 ${this._size.width} ${this._size.height}`;
2950
+ } else {
2951
+ this._viewbox = '0 0 0 0'; // Сбрасываем путь
2952
+ }
2953
+ }
2954
+
2955
+ /**
2956
+ * Возвращает строку viewBox.
2957
+ * @since 1.0.0
2958
+ * @protected
2959
+ * @returns {string}
2960
+ */
2961
+ _getViewbox() {
2962
+ return this._viewbox;
2963
+ }
2964
+
2965
+ /**
2966
+ * Применяет кривую, обновляя viewBox и d-атрибут пути.
2967
+ * @override
2968
+ * @since 1.0.0
2969
+ * @protected
2970
+ * @returns {void}
2971
+ */
2972
+ _applyCurve() {
2973
+ const svgLayer = this._virtualElementList.svgLayer;
2974
+ svgLayer.setAttribute('viewBox', this._getViewbox());
2975
+
2976
+ const svgLayerPath = this._virtualElementList.svgLayerPath;
2977
+ if (this._path) {
2978
+ svgLayerPath.setAttribute('d', this._path);
2979
+ } else {
2980
+ svgLayerPath.setAttribute('d', '');
2981
+ }
2982
+ }
2983
+
2984
+ /**
2985
+ * Восстанавливает исходный путь и очищает d-атрибут.
2986
+ * @override
2987
+ * @since 1.0.0
2988
+ * @protected
2989
+ * @returns {void}
2990
+ */
2991
+ _restoreCurve() {
2992
+ const svgLayerPath = this._virtualElementList.svgLayerPath;
2993
+
2994
+ svgLayerPath.setAttribute('d', '');
2995
+
2996
+ super._restoreCurve();
2997
+ }
2998
+
2999
+
3000
+ /**
3001
+ * =============================================================
3002
+ *
3003
+ * =============================================================
3004
+ */
3005
+ }
3006
+
3007
+ /**
3008
+ * @file src/mode-clip-path.js
3009
+ *
3010
+ * @module sj-superellipse/mode-clip-path
3011
+ * @since 1.0.0
3012
+ * @author f4n70m
3013
+ *
3014
+ * @description
3015
+ * Режим `clip-path` – самый лёгкий способ применения суперэллипса. Использует CSS-свойство `clip-path`
3016
+ * для обрезки элемента по форме суперэллипса. Не требует создания дополнительных DOM-узлов,
3017
+ * но не поддерживает корректное отображение теней (`box-shadow`), границ и сложных фонов.
3018
+ *
3019
+ * @extends SuperellipseMode
3020
+ * @example
3021
+ * const mode = new SuperellipseModeClipPath(element);
3022
+ * mode.activate();
3023
+ */
3024
+
3025
+
3026
+
3027
+ /**
3028
+ * Режим, использующий CSS-свойство `clip-path` для обрезки элемента.
3029
+ * Не требует создания дополнительных DOM-узлов, но не поддерживает тени, границы и сложные фоны.
3030
+ * @class SuperellipseModeClipPath
3031
+ * @extends SuperellipseMode
3032
+ * @since 1.0.0
3033
+ */
3034
+ class SuperellipseModeClipPath extends SuperellipseMode {
3035
+
3036
+
3037
+ /**
3038
+ * =============================================================
3039
+ * PUBLIC
3040
+ * =============================================================
3041
+ */
3042
+
3043
+ /**
3044
+ * Создаёт экземпляр режима clip-path.
3045
+ * @since 1.0.0
3046
+ * @param {Element} element - Целевой DOM-элемент.
3047
+ * @param {boolean} [debug=false] - Флаг отладки (передаётся в родительский класс).
3048
+ * @returns {SuperellipseModeClipPath} Экземпляр режима.
3049
+ */
3050
+ constructor(element, debug = false) {
3051
+ super(element, debug);
3052
+ }
3053
+
3054
+
3055
+ /**
3056
+ * =============================================================
3057
+ *
3058
+ * =============================================================
3059
+ */
3060
+ }
3061
+
3062
+ /**
3063
+ * @file src/controller.js
3064
+ *
3065
+ * @module sj-superellipse/controller
3066
+ * @since 1.0.0
3067
+ * @author f4n70m
3068
+ *
3069
+ * @description
3070
+ * Класс `SuperellipseController` – основной контроллер, связывающий DOM-элемент с выбранным режимом
3071
+ * (`svg-layer` или `clip-path`). Отслеживает изменения размеров, стилей, атрибутов и удаление элемента
3072
+ * через `ResizeObserver`, `MutationObserver`, `IntersectionObserver`. Управляет кэшированием стилей,
3073
+ * переключением режимов, активацией/деактивацией эффекта.
3074
+ *
3075
+ * @example
3076
+ * const controller = new SuperellipseController(element, { mode: 'svg-layer' });
3077
+ * controller.setCurveFactor(0.9);
3078
+ * controller.enable();
3079
+ */
3080
+
3081
+
3082
+
3083
+ /**
3084
+ * Контроллер, управляющий применением суперэллипса к DOM-элементу.
3085
+ */
3086
+ class SuperellipseController
3087
+ {
3088
+ _id;
3089
+ _debug;
3090
+
3091
+ _mode;
3092
+ _element;
3093
+
3094
+ _precision; // Количество знаков после запятой
3095
+ _curveFactor;
3096
+
3097
+ _mutationFrame;
3098
+ _resizeFrame;
3099
+ _intersectionFrame;
3100
+
3101
+
3102
+ _prepareTimer;
3103
+ _executeTimer;
3104
+ _isSelfMutation = false;
3105
+
3106
+ _resizeObserver;
3107
+ _mutationObserver;
3108
+ _removalObserver;
3109
+ _intersectionObserver;
3110
+
3111
+ _targetTriggers;
3112
+ _hoverHandlers;
3113
+
3114
+ _needsUpdate;
3115
+ _isSelfApply;
3116
+
3117
+
3118
+ _eventHandlers;
3119
+
3120
+
3121
+ /**
3122
+ * =============================================================
3123
+ * PUBLIC
3124
+ * =============================================================
3125
+ */
3126
+
3127
+
3128
+ /**
3129
+ * Создаёт экземпляр контроллера.
3130
+ * @since 1.0.0
3131
+ * @param {Element} element - Целевой DOM-элемент.
3132
+ * @param {Object} [options] - Опции инициализации.
3133
+ * @param {boolean} [options.force] - Принудительное пересоздание, если контроллер уже существует.
3134
+ * @param {string} [options.mode='svg-layer'] - Режим работы: 'svg-layer' или 'clip-path'.
3135
+ * @param {number} [options.curveFactor] - Коэффициент кривизны (диапазон -2..2).
3136
+ * @param {number} [options.precision=2] - Количество знаков после запятой в координатах пути.
3137
+ * @param {boolean} [options.debug=false] - Включить отладочный вывод.
3138
+ * @returns {SuperellipseController} Экземпляр контроллера.
3139
+ */
3140
+ constructor(element, options = {}) {
3141
+
3142
+ this._initId();
3143
+
3144
+ /** Проверка существующего контроллера **/
3145
+ if (this._inControllers() && !options.force) {
3146
+ jsse_console.warn({label:'CONTROLLER', element: element}, 'The element is already initialized. Use {force:true} to recreate it.');
3147
+ return this._getController();
3148
+ }
3149
+
3150
+ this._element = element;
3151
+
3152
+ /** Default options **/
3153
+ const settings = {
3154
+ mode: options.mode ?? 'svg-layer',
3155
+ debug: options.debug ?? false,
3156
+ curveFactor: options.curveFactor ?? jsse_getBorderRadiusFactor(),
3157
+ precision: options.precision ?? 2
3158
+ };
3159
+
3160
+ this._initDebug(settings.debug);
3161
+
3162
+ jsse_console.debug({label:'CONTROLLER',element:this._element}, '[SETTINGS]', settings);
3163
+
3164
+
3165
+ this._curveFactor = settings.curveFactor;
3166
+ this._precision = settings.precision;
3167
+
3168
+
3169
+ this._needsUpdate = false;
3170
+ this._isSelfApply = false;
3171
+
3172
+ /** Слушатели **/
3173
+ this._resizeObserver = null;
3174
+ this._mutationObserver = null;
3175
+ this._removalObserver = null;
3176
+ this._intersectionObserver = null;
3177
+
3178
+ /** init **/
3179
+ this._initEvents();
3180
+ this._initCacheStyles();
3181
+ this._setInitiatedAttr();
3182
+
3183
+ this._setMode(settings.mode);
3184
+ this._activateMode();
3185
+ this._connectObservers();
3186
+ }
3187
+
3188
+ /**
3189
+ * Переключает режим работы.
3190
+ * @since 1.0.0
3191
+ * @param {string} modeName - Имя режима ('svg-layer' или 'clip-path').
3192
+ * @returns {SuperellipseController} this (для цепочек вызовов).
3193
+ */
3194
+ switchMode(modeName) {
3195
+ this._deactivateMode();
3196
+ this._unsetMode();
3197
+ this._setMode(modeName);
3198
+ this._activateMode();
3199
+ return this;
3200
+ }
3201
+
3202
+ /**
3203
+ * Проверяет, активирован ли суперэллипс.
3204
+ * @since 1.0.0
3205
+ * @returns {boolean} true, если эффект активен.
3206
+ */
3207
+ isEnabled() {
3208
+ return this._mode.isActivated();
3209
+ }
3210
+
3211
+ /**
3212
+ * Активирует суперэллипс.
3213
+ * @since 1.0.0
3214
+ * @returns {SuperellipseController} this.
3215
+ */
3216
+ enable() {
3217
+ this._activateMode();
3218
+ return this;
3219
+ }
3220
+
3221
+ /**
3222
+ * Деактивирует суперэллипс, восстанавливая исходные стили.
3223
+ * @since 1.0.0
3224
+ * @returns {Element} Целевой элемент.
3225
+ */
3226
+ disable() {
3227
+ this._deactivateMode();
3228
+ return this;
3229
+ }
3230
+
3231
+ /**
3232
+ * Устанавливает коэффициент кривизны углов.
3233
+ * @since 1.0.0
3234
+ * @param {number} value - Новое значение (диапазон -2..2).
3235
+ * @returns {SuperellipseController} this.
3236
+ */
3237
+ setCurveFactor(value) {
3238
+ this._curveFactor = value;
3239
+ this._mode.updateCurveFactor(value);
3240
+ this._emit('update', { type: 'curveFactor' });
3241
+ return this;
3242
+ }
3243
+
3244
+ /**
3245
+ * Устанавливает точность округления координат пути.
3246
+ * @since 1.0.0
3247
+ * @param {number} value - Количество знаков после запятой.
3248
+ * @returns {SuperellipseController} this.
3249
+ */
3250
+ setPrecision(value) {
3251
+ this._precision = value;
3252
+ this._mode.updatePrecision(value);
3253
+ return this;
3254
+ }
3255
+
3256
+ /**
3257
+ * Возвращает текущий SVG-путь суперэллипса.
3258
+ * @since 1.0.0
3259
+ * @returns {string} Строка с командами path.
3260
+ */
3261
+ getPath() {
3262
+ return this._mode.getPath();
3263
+ }
3264
+
3265
+ /**
3266
+ * Полностью уничтожает контроллер и удаляет все связанные эффекты.
3267
+ * @since 1.0.0
3268
+ * @returns {Element} Целевой элемент.
3269
+ */
3270
+ destroy() {
3271
+ return this._destroyController();
3272
+ }
3273
+
3274
+
3275
+ /**
3276
+ * =============================================================
3277
+ * PRIVATE
3278
+ * =============================================================
3279
+ */
3280
+
3281
+
3282
+ /**
3283
+ * Инициализирует уникальный идентификатор контроллера.
3284
+ * @since 1.0.0
3285
+ * @protected
3286
+ * @returns {void}
3287
+ */
3288
+ _initId() {
3289
+ this._id = jsse_counter.value;
3290
+ jsse_counter.increment();
3291
+ }
3292
+
3293
+ /**
3294
+ * Инициализирует флаг отладки.
3295
+ * @since 1.0.0
3296
+ * @protected
3297
+ * @param {boolean} debug - Включить отладку.
3298
+ * @returns {void}
3299
+ */
3300
+ _initDebug(debug) {
3301
+ this._debug = !!debug;
3302
+ if (this._debug) {
3303
+ jsse_console.set(this._element);
3304
+ }
3305
+ }
3306
+
3307
+ /**
3308
+ * Проверяет, включён ли режим отладки для данного контроллера.
3309
+ * @since 1.0.0
3310
+ * @protected
3311
+ * @returns {boolean}
3312
+ */
3313
+ _isDebug() {
3314
+ return this._debug;
3315
+ }
3316
+
3317
+ /**
3318
+ * Проверяет, не скрыт ли элемент (`display: none`).
3319
+ * @since 1.0.0
3320
+ * @protected
3321
+ * @returns {boolean}
3322
+ */
3323
+ _isDisplay() {
3324
+ const capturedStyles = getComputedStyle(this._element);
3325
+ return capturedStyles.getPropertyValue('display') !== 'none';
3326
+ }
3327
+
3328
+ /**
3329
+ * Полное уничтожение контроллера (внутренняя логика).
3330
+ * @since 1.0.0
3331
+ * @protected
3332
+ * @returns {void}
3333
+ */
3334
+ _destroyController() {
3335
+ this._disconnectObservers();
3336
+ this._deactivateMode();
3337
+ this._unsetMode();
3338
+ this._removeInitiatedAttr();
3339
+
3340
+ this._destroyStylesheet();
3341
+
3342
+ this._deleteCacheStyles();
3343
+ this._deleteFromControllers();
3344
+ }
3345
+
3346
+
3347
+ /**
3348
+ * =============================================================
3349
+ * EVENTS API
3350
+ * =============================================================
3351
+ */
3352
+
3353
+ /**
3354
+ * Инициализирует систему событий.
3355
+ * @since 1.2.0
3356
+ * @protected
3357
+ * @returns {void}
3358
+ */
3359
+ _initEvents() {
3360
+ this._eventHandlers = {
3361
+ update: [],
3362
+ activate: [],
3363
+ deactivate: [],
3364
+ error: []
3365
+ };
3366
+ };
3367
+
3368
+ /**
3369
+ * Подписывается на событие контроллера.
3370
+ * @since 1.2.0
3371
+ * @param {string} event - Имя события ('update', 'activate', 'deactivate', 'error').
3372
+ * @param {Function} callback - Функция-обработчик. Принимает объект события.
3373
+ * @returns {SuperellipseController} this.
3374
+ */
3375
+ on(event, callback) {
3376
+ if (this._eventHandlers[event]) {
3377
+ this._eventHandlers[event].push(callback);
3378
+ }
3379
+ return this;
3380
+ }
3381
+
3382
+ /**
3383
+ * Отписывается от события контроллера.
3384
+ * @since 1.2.0
3385
+ * @param {string} event - Имя события.
3386
+ * @param {Function} callback - Ранее добавленный обработчик.
3387
+ * @returns {SuperellipseController} this.
3388
+ */
3389
+ off(event, callback) {
3390
+ if (this._eventHandlers[event]) {
3391
+ const index = this._eventHandlers[event].indexOf(callback);
3392
+ if (index !== -1) this._eventHandlers[event].splice(index, 1);
3393
+ }
3394
+ return this;
3395
+ }
3396
+
3397
+ /**
3398
+ * Вызывает событие с заданными данными.
3399
+ * @since 1.2.0
3400
+ * @protected
3401
+ * @param {string} event - Имя события.
3402
+ * @param {*} data - Данные события.
3403
+ * @returns {void}
3404
+ */
3405
+ _emit(event, data) {
3406
+ if (this._eventHandlers[event]) {
3407
+ this._eventHandlers[event].forEach(cb => {
3408
+ try {
3409
+ cb({ type: event, data, timestamp: Date.now(), target: this._element });
3410
+ } catch (e) {
3411
+ console.error('[JSSE] Event handler error:', e);
3412
+ }
3413
+ });
3414
+ }
3415
+ }
3416
+
3417
+
3418
+ /**
3419
+ * =============================================================
3420
+ * MODE
3421
+ * =============================================================
3422
+ */
3423
+
3424
+
3425
+ /**
3426
+ * Устанавливает активный режим.
3427
+ * @since 1.0.0
3428
+ * @protected
3429
+ * @param {string} modeName - Имя режима ('svg-layer' или 'clip-path').
3430
+ * @returns {void}
3431
+ */
3432
+ _setMode(modeName) {
3433
+ switch (modeName) {
3434
+ case 'svg-layer':
3435
+ this._mode = new SuperellipseModeSvgLayer(this._element);
3436
+ break;
3437
+
3438
+ case 'clip-path':
3439
+ default:
3440
+ this._mode = new SuperellipseModeClipPath(this._element);
3441
+ break;
3442
+ }
3443
+ this._mode.setCurveFactor(this._curveFactor);
3444
+ this._mode.setPrecision(this._precision);
3445
+
3446
+ }
3447
+
3448
+ /**
3449
+ * Удаляет текущий режим, вызывая его деструктор.
3450
+ * @since 1.0.0
3451
+ * @protected
3452
+ * @returns {void}
3453
+ */
3454
+ _unsetMode() {
3455
+
3456
+ this._mode.destroy();
3457
+ this._mode = null;
3458
+ }
3459
+
3460
+ /**
3461
+ * Активирует текущий режим и инициализирует обработчики hover.
3462
+ * @since 1.0.0
3463
+ * @protected
3464
+ * @returns {void}
3465
+ */
3466
+ _activateMode() {
3467
+ this._mode.activate();
3468
+ this._initStylesheet();
3469
+ this._registerTargetListeners(this._targetTriggers);
3470
+
3471
+ this._emit('activate', { mode: this._mode._getModeName() });
3472
+ }
3473
+
3474
+ /**
3475
+ * Деактивирует текущий режим и удаляет обработчики hover.
3476
+ * @since 1.0.0
3477
+ * @protected
3478
+ * @returns {void}
3479
+ */
3480
+ _deactivateMode() {
3481
+ this._mode.deactivate();
3482
+ this._unregisterTargetListeners();
3483
+
3484
+ this._emit('deactivate', { mode: this._mode._getModeName() });
3485
+ }
3486
+
3487
+
3488
+ /**
3489
+ * =============================================================
3490
+ * ATTRIBUTES
3491
+ * =============================================================
3492
+ */
3493
+
3494
+
3495
+ /**
3496
+ * Присваивает элементу атрибут `data-jsse-initiated`.
3497
+ * @since 1.0.0
3498
+ * @protected
3499
+ * @returns {void}
3500
+ */
3501
+ _setInitiatedAttr() {
3502
+ this._element.setAttribute('data-jsse-initiated', true);
3503
+ }
3504
+
3505
+ /**
3506
+ * Удаляет атрибут `data-jsse-initiated`.
3507
+ * @since 1.0.0
3508
+ * @protected
3509
+ * @returns {void}
3510
+ */
3511
+ _removeInitiatedAttr() {
3512
+ this._element.removeAttribute('data-jsse-initiated');
3513
+ }
3514
+
3515
+
3516
+ /**
3517
+ * =============================================================
3518
+ * CACHE
3519
+ * =============================================================
3520
+ */
3521
+
3522
+
3523
+ /**
3524
+ * Инициализирует кэш стилей для элемента.
3525
+ * @since 1.0.0
3526
+ * @protected
3527
+ * @returns {void}
3528
+ */
3529
+ _initCacheStyles() {
3530
+ if (!jsse_styles.get(this._element)) {
3531
+ jsse_styles.set(this._element, {});
3532
+ }
3533
+ }
3534
+
3535
+ /**
3536
+ * Удаляет кэш стилей элемента.
3537
+ * @since 1.0.0
3538
+ * @protected
3539
+ * @returns {void}
3540
+ */
3541
+ _deleteCacheStyles() {
3542
+ jsse_styles.delete(this._element);
3543
+ }
3544
+
3545
+ /**
3546
+ * Получает контроллер, связанный с элементом (из глобальной WeakMap).
3547
+ * @since 1.0.0
3548
+ * @protected
3549
+ * @returns {SuperellipseController|undefined}
3550
+ */
3551
+ _getController() {
3552
+ return jsse_controllers.get(this._element);
3553
+ }
3554
+
3555
+ /**
3556
+ * Проверяет, существует ли контроллер для элемента.
3557
+ * @since 1.0.0
3558
+ * @protected
3559
+ * @returns {boolean}
3560
+ */
3561
+ _inControllers() {
3562
+ return !!this._getController();
3563
+ }
3564
+
3565
+ /**
3566
+ * Удаляет ссылку на контроллер из глобальной WeakMap.
3567
+ * @since 1.0.0
3568
+ * @protected
3569
+ * @returns {void}
3570
+ */
3571
+ _deleteFromControllers() {
3572
+ jsse_controllers.delete(this._element);
3573
+ }
3574
+
3575
+
3576
+ /**
3577
+ * =============================================================
3578
+ * STYLESHEET
3579
+ * =============================================================
3580
+ */
3581
+
3582
+ /**
3583
+ * Инициализирует парсинг стилей и находит триггеры для hover.
3584
+ * @since 1.1.0
3585
+ * @protected
3586
+ * @returns {void}
3587
+ */
3588
+ _initStylesheet() {
3589
+ this._targetTriggers = this._getTargetTriggers();
3590
+ jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TARGET]', '[INIT]');
3591
+ }
3592
+
3593
+ /**
3594
+ * Уничтожает данные стилей и обработчики hover.
3595
+ * @since 1.1.0
3596
+ * @protected
3597
+ * @returns {void}
3598
+ */
3599
+ _destroyStylesheet() {
3600
+ this._targetTriggers = null;
3601
+ this._hoverHandlers = null;
3602
+ this._unregisterGlobalTouchEndListener();
3603
+ jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TARGET]', '[DESTROY]');
3604
+ }
3605
+
3606
+ /**
3607
+ * Регистрирует обработчики событий на элементах-триггерах.
3608
+ * @since 1.1.0
3609
+ * @protected
3610
+ * @param {Object} triggers - Объект, где ключ – селектор, значение – массив элементов.
3611
+ * @returns {void}
3612
+ */
3613
+ _registerTargetListeners(triggers) {
3614
+ this._hoverHandlers = {};
3615
+ for (const selector in triggers) {
3616
+ this._hoverHandlers[selector] = {
3617
+ in : (event) => { this._triggerHandlerIn(selector, event); },
3618
+ out : (event) => { this._triggerHandlerOut(selector, event); },
3619
+ touchStart : (event) => { this._triggerHandlerTouchIn(selector, event); },
3620
+ on : [],
3621
+ hovered : false,
3622
+ touchCount : 0
3623
+ };
3624
+ triggers[selector].forEach((trigger) => {
3625
+ this._hoverHandlers[selector].on.push(trigger);
3626
+ this._registerTriggerListener(trigger, selector);
3627
+ });
3628
+ }
3629
+ jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TARGET]', '[EVENTS]', true);
3630
+ }
3631
+
3632
+ /**
3633
+ * Удаляет все зарегистрированные обработчики с триггеров.
3634
+ * @since 1.1.0
3635
+ * @protected
3636
+ * @returns {void}
3637
+ */
3638
+ _unregisterTargetListeners() {
3639
+ for (const selector in this._hoverHandlers) {
3640
+ for (const trigger of this._hoverHandlers[selector].on) {
3641
+ if (trigger && trigger.removeEventListener) {
3642
+ this._unregisterTriggerListener(trigger, selector);
3643
+ }
3644
+ }
3645
+ }
3646
+ this._hoverHandlers = {};
3647
+
3648
+ // Удаляем глобальный обработчик touchend
3649
+ if (this._globalTouchEndHandler) {
3650
+ document.body.removeEventListener('touchend', this._globalTouchEndHandler);
3651
+ this._globalTouchEndHandler = null;
3652
+ }
3653
+
3654
+ jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TARGET]', '[EVENTS]', false);
3655
+ }
3656
+
3657
+ /**
3658
+ * Добавляет обработчики на конкретный элемент-триггер.
3659
+ * @since 1.1.0
3660
+ * @protected
3661
+ * @param {Element} trigger - DOM-элемент-триггер.
3662
+ * @param {string} selector - Селектор, связанный с триггером.
3663
+ * @returns {void}
3664
+ */
3665
+ _registerTriggerListener(trigger, selector) {
3666
+ // Мышь / перо
3667
+ trigger.addEventListener('mouseenter', this._hoverHandlers[selector].in);
3668
+ trigger.addEventListener('mouseleave', this._hoverHandlers[selector].out);
3669
+
3670
+ // Касание
3671
+ trigger.addEventListener('touchstart', this._hoverHandlers[selector].touchStart);
3672
+ this._registerGlobalTouchEndListener();
3673
+
3674
+ jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TRIGGER]', '[EVENT]', true, selector);
3675
+ }
3676
+
3677
+ /**
3678
+ * Регистрирует глобальный обработчик `touchend` для корректной работы hover на сенсорных экранах.
3679
+ * @since 1.1.0
3680
+ * @protected
3681
+ * @returns {void}
3682
+ */
3683
+ _registerGlobalTouchEndListener() {
3684
+ // Глобальный обработчик touchend (добавляем один раз)
3685
+ if (!this._globalTouchEndHandler) {
3686
+ this._globalTouchEndHandler = (event) => {
3687
+ for (const selector in this._hoverHandlers) {
3688
+ const handler = this._hoverHandlers[selector];
3689
+ if (handler.touchCount > 0) {
3690
+ this._triggerHandlerTouchOut(selector, event);
3691
+ }
3692
+ }
3693
+ };
3694
+ document.body.addEventListener('touchend', this._globalTouchEndHandler);
3695
+ }
3696
+ }
3697
+
3698
+ /**
3699
+ * Удаляет глобальный обработчик `touchend`.
3700
+ * @since 1.1.0
3701
+ * @protected
3702
+ * @returns {void}
3703
+ */
3704
+ _unregisterGlobalTouchEndListener() {
3705
+ if (this._globalTouchEndHandler) {
3706
+ document.body.removeEventListener('touchend', this._globalTouchEndHandler);
3707
+ this._globalTouchEndHandler = null;
3708
+ }
3709
+ }
3710
+
3711
+ /**
3712
+ * Удаляет обработчики с элемента-триггера.
3713
+ * @since 1.1.0
3714
+ * @protected
3715
+ * @param {Element} trigger - DOM-элемент-триггер.
3716
+ * @param {string} selector - Селектор, связанный с триггером.
3717
+ * @returns {void}
3718
+ */
3719
+ _unregisterTriggerListener(trigger, selector) {
3720
+ trigger.removeEventListener('mouseenter', this._hoverHandlers[selector].in);
3721
+ trigger.removeEventListener('mouseleave', this._hoverHandlers[selector].out);
3722
+ trigger.removeEventListener('touchstart', this._hoverHandlers[selector].touchStart);
3723
+
3724
+ jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TRIGGER]', '[EVENT]', false, selector);
3725
+ }
3726
+
3727
+ /**
3728
+ * Обработчик события `mouseenter` / `pointerenter` на триггере.
3729
+ * @since 1.1.0
3730
+ * @protected
3731
+ * @param {string} selector - Селектор триггера.
3732
+ * @param {Event} event - Событие.
3733
+ * @returns {void}
3734
+ */
3735
+ _triggerHandlerIn(selector, event) {
3736
+ if ( !this._element.matches(selector) || !this._hoverHandlers[selector] ) return;
3737
+ this._hoverHandlers[selector].hovered = true;
3738
+ jsse_console.debug({label:'HOVER',element:this._element}, '[IN]', selector, event);
3739
+ this._mutationHandler();
3740
+ }
3741
+
3742
+ /**
3743
+ * Обработчик события `mouseleave` / `pointerleave` на триггере.
3744
+ * @since 1.1.0
3745
+ * @protected
3746
+ * @param {string} selector - Селектор триггера.
3747
+ * @param {Event} event - Событие.
3748
+ * @returns {void}
3749
+ */
3750
+ _triggerHandlerOut(selector, event) {
3751
+ if ( this._element.matches(selector) || !this._hoverHandlers[selector] || !this._hoverHandlers[selector]?.hovered ) return;
3752
+ this._hoverHandlers[selector].hovered = false;
3753
+ jsse_console.debug({label:'HOVER',element:this._element}, '[OUT]', selector, event);
3754
+ this._mutationHandler();
3755
+ }
3756
+
3757
+ /**
3758
+ * Обработчик `touchstart` на триггере (увеличивает счётчик касаний).
3759
+ * @since 1.1.0
3760
+ * @protected
3761
+ * @param {string} selector - Селектор триггера.
3762
+ * @param {TouchEvent} event - Событие касания.
3763
+ * @returns {void}
3764
+ */
3765
+ _triggerHandlerTouchIn(selector, event) {
3766
+ if ( !this._hoverHandlers[selector] || this._hoverHandlers[selector]?.touchCount !== undefined ) {
3767
+ this._hoverHandlers[selector].touchCount++;
3768
+ if (this._hoverHandlers[selector].touchCount === 1) {
3769
+ this._triggerHandlerIn(selector, event);
3770
+ }
3771
+ }
3772
+ }
3773
+
3774
+ /**
3775
+ * Обработчик `touchend` (уменьшает счётчик касаний и вызывает выход при обнулении).
3776
+ * @since 1.1.0
3777
+ * @protected
3778
+ * @param {string} selector - Селектор триггера.
3779
+ * @param {TouchEvent} event - Событие касания.
3780
+ * @returns {void}
3781
+ */
3782
+ _triggerHandlerTouchOut(selector, event) {
3783
+ if ( !this._hoverHandlers[selector] || this._hoverHandlers[selector]?.touchCount !== undefined ) {
3784
+ this._hoverHandlers[selector].touchCount--;
3785
+ if (this._hoverHandlers[selector].touchCount === 0) {
3786
+ this._triggerHandlerOut(selector, event);
3787
+ }
3788
+ }
3789
+ }
3790
+
3791
+ /**
3792
+ * Находит все элементы-триггеры, которые могут вызвать изменение стилей при наведении на целевой элемент.
3793
+ * @since 1.1.0
3794
+ * @protected
3795
+ * @returns {Object<string, Element[]>} Объект, где ключ – селектор, значение – массив элементов.
3796
+ */
3797
+ _getTargetTriggers() {
3798
+ const triggerList = {};
3799
+ const targetSelectors = jsse_stylesheet.getTargetSelectors(this._element, {selectorHasHover:true});
3800
+ for (const targetSelector of targetSelectors) {
3801
+ const selectorTargetElements = this._getSelectorTriggerElements(targetSelector);
3802
+ if (selectorTargetElements.length > 0) {
3803
+ const selector = targetSelector.getSelector();
3804
+ triggerList[selector] = selectorTargetElements;
3805
+ }
3806
+ }
3807
+ return triggerList;
3808
+ }
3809
+
3810
+ /**
3811
+ * Для заданного CSS-правила (селектора) возвращает массив элементов-триггеров.
3812
+ * @since 1.1.0
3813
+ * @protected
3814
+ * @param {StylesheetParserSelector} selector - Объект селектора.
3815
+ * @returns {Element[]}
3816
+ */
3817
+ _getSelectorTriggerElements(selector) {
3818
+ const selectorTargetElements = [];
3819
+ const selectorParts = selector.getTriggerParts();
3820
+ for (const selectorPart of selectorParts) {
3821
+ const triggerElements = this._getSelectorPartTriggerElements(selectorPart);
3822
+ selectorTargetElements.push(...triggerElements);
3823
+ }
3824
+ return selectorTargetElements;
3825
+ }
3826
+
3827
+ /**
3828
+ * Для одной части составного селектора возвращает элементы-триггеры.
3829
+ * @since 1.1.0
3830
+ * @protected
3831
+ * @param {Object} selectorPart - Часть селектора с полями parent, neighbor, child.
3832
+ * @returns {Element[]}
3833
+ */
3834
+ _getSelectorPartTriggerElements(selectorPart) {
3835
+ if (selectorPart.neighbor) {
3836
+ const neighborSelector = `${selectorPart.neighbor.combinator}${selectorPart.neighbor.clean}`;
3837
+ const cssSelectorHasCombinator = `:has(${selectorPart.neighbor.combinator}*)`;
3838
+ const hasCombinatorIsSupport = jsse_css_selector.isSupport(cssSelectorHasCombinator);
3839
+ if (hasCombinatorIsSupport) {
3840
+ return this._getSelectorPartTriggerElementsWithHasSupport(selectorPart, neighborSelector);
3841
+ } else {
3842
+ // Браузер НЕ поддерживает :has() — используем fallback
3843
+ jsse_console.warn({label:'HOVER', element: this._element}, '[FALLBACK] Using manual DOM traversal for:', neighborSelector);
3844
+ return this._getSelectorPartTriggerElementsWithoutHasSupport(selectorPart.parent, neighborSelector, selectorPart.child);
3845
+ }
3846
+ } else {
3847
+ // Нет соседа — обычный селектор
3848
+ const triggers = Array.from(document.querySelectorAll(selectorPart.parent));
3849
+ return triggers.filter(trigger =>
3850
+ this._elementMatchesChildSelector(trigger, selectorPart.child)
3851
+ );
3852
+ }
3853
+ }
3854
+
3855
+ /**
3856
+ * Реализация поиска триггеров с использованием современного CSS-селектора `:has()`.
3857
+ * @since 1.1.0
3858
+ * @protected
3859
+ * @param {Object} selectorPart - Часть селектора.
3860
+ * @param {string} neighborSelector - Селектор соседнего элемента.
3861
+ * @returns {Element[]}
3862
+ */
3863
+ _getSelectorPartTriggerElementsWithHasSupport(selectorPart, neighborSelector) {
3864
+ // Браузер поддерживает :has() — используем быстрый селектор
3865
+ const triggersSelector = `${selectorPart.parent}:has(${neighborSelector})`;
3866
+ const siblingSelector = `${selectorPart.parent}${neighborSelector}`;
3867
+
3868
+ const triggers = Array.from(document.querySelectorAll(triggersSelector));
3869
+ const siblings = Array.from(document.querySelectorAll(siblingSelector));
3870
+
3871
+ return triggers.filter((trigger, index) => {
3872
+ const current = siblings[index];
3873
+ return this._elementMatchesChildSelector(current, selectorPart.child);
3874
+ });
3875
+ }
3876
+
3877
+ /**
3878
+ * Fallback-реализация поиска триггеров для браузеров без поддержки `:has()`.
3879
+ * @since 1.1.0
3880
+ * @protected
3881
+ * @param {string} parentSelector - Селектор родителя.
3882
+ * @param {string} neighborSelector - Селектор соседа (с комбинатором).
3883
+ * @param {string} childSelector - Селектор дочернего элемента (целевой элемент).
3884
+ * @returns {Element[]}
3885
+ */
3886
+ _getSelectorPartTriggerElementsWithoutHasSupport(parentSelector, neighborSelector, childSelector) {
3887
+ const result = [];
3888
+
3889
+ // 1. Находим всех потенциальных родителей
3890
+ const allParents = Array.from(document.querySelectorAll(parentSelector));
3891
+
3892
+ // 2. Парсим комбинатор и чистый селектор соседа
3893
+ const combinator = neighborSelector.trim()[0]; // '+' или '~'
3894
+ const cleanNeighborSelector = neighborSelector.trim().substring(1).trim();
3895
+
3896
+ for (const parent of allParents) {
3897
+ // 3. Ищем соседние элементы относительно родителя или внутри него
3898
+ let neighborElements = [];
3899
+
3900
+ if (combinator === '+') {
3901
+ // Соседний элемент (сразу следующий)
3902
+ const nextSibling = parent.nextElementSibling;
3903
+ if (nextSibling && nextSibling.matches(cleanNeighborSelector)) {
3904
+ neighborElements = [nextSibling];
3905
+ }
3906
+ } else if (combinator === '~') {
3907
+ // Все последующие соседние элементы
3908
+ let sibling = parent.nextElementSibling;
3909
+ while (sibling) {
3910
+ if (sibling.matches(cleanNeighborSelector)) {
3911
+ neighborElements.push(sibling);
3912
+ }
3913
+ sibling = sibling.nextElementSibling;
3914
+ }
3915
+ }
3916
+
3917
+ // 4. Проверяем, содержит ли найденный сосед целевой элемент
3918
+ for (const neighbor of neighborElements) {
3919
+ if (this._elementMatchesChildSelector(neighbor, childSelector)) {
3920
+ result.push(parent);
3921
+ break; // Нашли триггер для этого родителя
3922
+ }
3923
+ }
3924
+ }
3925
+
3926
+ return result;
3927
+ }
3928
+
3929
+ /**
3930
+ * Возвращает список элементов, соответствующих селектору, в заданном контексте.
3931
+ * @since 1.1.0
3932
+ * @protected
3933
+ * @param {string} selector - CSS-селектор.
3934
+ * @param {Element|Document} [parent=document] - Корневой элемент для поиска.
3935
+ * @returns {NodeListOf<Element>}
3936
+ */
3937
+ _getSelectorElements(selector, parent=document) {
3938
+ return parent.querySelectorAll(selector);
3939
+ }
3940
+
3941
+ /**
3942
+ * Проверяет, содержится ли целевой элемент внутри родителя с учётом дочернего селектора.
3943
+ * @since 1.1.0
3944
+ * @protected
3945
+ * @param {Element} parent - Потенциальный родитель.
3946
+ * @param {string} selector - Селектор дочернего элемента.
3947
+ * @returns {boolean}
3948
+ */
3949
+ _elementMatchesChildSelector(parent, selector) {
3950
+ if (!(parent.contains(this._element) || parent === this._element)) {
3951
+ return false;
3952
+ }
3953
+ if (parent === this._element) return true;
3954
+
3955
+ const children = this._getSelectorElements(selector, parent);
3956
+ return Array.from(children).includes(this._element);
3957
+ }
3958
+
3959
+ /**
3960
+ * Заглушка для получения элементов-триггеров (не используется).
3961
+ * @since 1.1.0
3962
+ * @protected
3963
+ * @returns {void}
3964
+ */
3965
+ _getTriggerElements() {
3966
+ jsse_stylesheet.getTargetSelectors(this._element, {selectorHasHover:true});
3967
+ }
3968
+
3969
+
3970
+ /**
3971
+ * =============================================================
3972
+ * OBSERVERS
3973
+ * =============================================================
3974
+ */
3975
+
3976
+ /**
3977
+ * Подключает наблюдатели: MutationObserver, ResizeObserver, IntersectionObserver и отслеживание удаления.
3978
+ * @since 1.0.0
3979
+ * @protected
3980
+ * @returns {void}
3981
+ */
3982
+ _connectObservers() {
3983
+ this._mutationObserver = new MutationObserver(() => {
3984
+ this._mutationHandler();
3985
+ });
3986
+ this._mutationObserver.observe(this._element, {
3987
+ attributes: true,
3988
+ attributeFilter: ['style', 'class']
3989
+ });
3990
+
3991
+ if (typeof IntersectionObserver !== 'undefined') {
3992
+ this._intersectionObserver = new IntersectionObserver((entries) => {
3993
+ this._intersectionHandler(entries);
3994
+ });
3995
+ this._intersectionObserver.observe(this._element);
3996
+ }
3997
+
3998
+ if (typeof ResizeObserver !== 'undefined') {
3999
+ this._resizeObserver = new ResizeObserver(() => {
4000
+ this._resizeHandler();
4001
+ });
4002
+ this._resizeObserver.observe(this._element);
4003
+ }
4004
+
4005
+ this._removalObserver = new MutationObserver(() => {
4006
+ this._destroyHandler();
4007
+ });
4008
+ this._removalObserver.observe(document.body, {
4009
+ childList: true,
4010
+ subtree: true
4011
+ });
4012
+ }
4013
+
4014
+ /**
4015
+ * Отключает всех наблюдателей и очищает таймеры.
4016
+ * @since 1.0.0
4017
+ * @protected
4018
+ * @returns {void}
4019
+ */
4020
+ _disconnectObservers() {
4021
+ if (this._prepareTimer) clearTimeout(this._prepareTimer);
4022
+ if (this._executeTimer) clearTimeout(this._executeTimer);
4023
+
4024
+ if (this._resizeObserver) this._resizeObserver.disconnect();
4025
+ if (this._mutationObserver) this._mutationObserver.disconnect();
4026
+ if (this._intersectionObserver) this._intersectionObserver.disconnect();
4027
+ if (this._removalObserver) this._removalObserver.disconnect();
4028
+ }
4029
+
4030
+ /**
4031
+ * Обработчик мутаций (изменение атрибутов style/class). Запускает отложенное обновление.
4032
+ * @since 1.0.0
4033
+ * @protected
4034
+ * @returns {void}
4035
+ */
4036
+ _mutationHandler() {
4037
+ jsse_console.debug({label:'MUTATION', element:this._element}, '[DETECT]', this._isSelfMutation ? 'self' : 'flow');
4038
+ if (this._isSelfMutation)
4039
+ return;
4040
+ if (this._prepareTimer !== null) {
4041
+ clearTimeout(this._prepareTimer);
4042
+ }
4043
+ this._prepareTimer = setTimeout(() => {
4044
+ this._prepareTimer = null;
4045
+ jsse_console.debug({label:'MUTATION', element:this._element}, '[START]');
4046
+ this._isSelfMutation = true;
4047
+ try {
4048
+ jsse_console.debug({label:'MUTATION', element:this._element}, '[UPDATE]');
4049
+ if (this._isDisplay() && this._needsUpdate) {
4050
+ this._mode.update();
4051
+ this._emit('update', { type: 'full' });
4052
+ this._needsUpdate = false;
4053
+ } else {
4054
+ this._mode.updateStyles();
4055
+ this._emit('update', { type: 'styles' });
4056
+ }
4057
+ } finally {
4058
+ if (this._executeTimer !== null) {
4059
+ clearTimeout(this._executeTimer);
4060
+ }
4061
+ this._executeTimer = setTimeout(() => {
4062
+ this._executeTimer = null;
4063
+ jsse_console.debug({label:'MUTATION', element:this._element}, '[END]');
4064
+ this._isSelfMutation = false;
4065
+
4066
+ }, 0);
4067
+ }
4068
+ }, 0);
4069
+ }
4070
+
4071
+ /**
4072
+ * Обработчик изменения размеров элемента. При скрытом элементе устанавливает флаг `_needsUpdate`.
4073
+ * @since 1.0.0
4074
+ * @protected
4075
+ * @returns {void}
4076
+ */
4077
+ _resizeHandler() {
4078
+ if (this._isDisplay()) {
4079
+ try {
4080
+ this._mode.updateSize();
4081
+ this._emit('update', { type: 'size' });
4082
+ } finally {
4083
+ }
4084
+ } else {
4085
+ this._needsUpdate = true;
4086
+ }
4087
+ }
4088
+
4089
+ /**
4090
+ * Обработчик видимости элемента (IntersectionObserver). При появлении элемента выполняет отложенное обновление.
4091
+ * @since 1.0.0
4092
+ * @protected
4093
+ * @param {IntersectionObserverEntry[]} entries - Записи пересечений.
4094
+ * @returns {void}
4095
+ */
4096
+ _intersectionHandler(entries) {
4097
+ if (entries[0].isIntersecting && this._needsUpdate) {
4098
+ try {
4099
+ this._mode.update();
4100
+ this._emit('update', { type: 'full' });
4101
+ this._needsUpdate = false;
4102
+ } finally {
4103
+ }
4104
+ }
4105
+ }
4106
+
4107
+ /**
4108
+ * Обработчик удаления элемента из DOM. При отсутствии элемента в документе уничтожает контроллер.
4109
+ * @since 1.0.0
4110
+ * @protected
4111
+ * @returns {void}
4112
+ */
4113
+ _destroyHandler() {
4114
+ if (!document.body.contains(this._element)) {
4115
+ this._destroyController();
4116
+ }
4117
+ }
4118
+ }
4119
+
4120
+ /**
4121
+ * @file src/api.js
4122
+ *
4123
+ * @module sj-superellipse/api
4124
+ * @since 1.0.0
4125
+ * @author f4n70m
4126
+ *
4127
+ * @description
4128
+ * Публичное API библиотеки. Определяет глобальную функцию инициализации `superellipseInit`,
4129
+ * а также расширяет `Element.prototype` методами `superellipse` (геттер) и `superellipseInit`.
4130
+ * Управляет слабой картой контроллеров `jsse_controllers`.
4131
+ *
4132
+ * @example
4133
+ * // Инициализация через глобальную функцию
4134
+ * const controller = superellipseInit('.card', { curveFactor: 1.2 });
4135
+ *
4136
+ * @example
4137
+ * // Инициализация через метод элемента
4138
+ * const el = document.querySelector('.card');
4139
+ * el.superellipseInit({ mode: 'clip-path' });
4140
+ * const controller = el.superellipse;
4141
+ */
4142
+
4143
+
4144
+ /**
4145
+ * Геттер для доступа к контроллеру через element.superellipse.
4146
+ * @name Element.prototype.superellipse
4147
+ * @function
4148
+ * @returns {SuperellipseController|undefined} Контроллер, если элемент инициализирован, иначе undefined.
4149
+ */
4150
+ Object.defineProperty(Element.prototype, 'superellipse', {
4151
+ get() {
4152
+ return jsse_controllers.get(this);
4153
+ }
4154
+ });
4155
+
4156
+
4157
+ /**
4158
+ * Инициализирует контроллер суперэллипса на DOM-элементе.
4159
+ * @name Element.prototype.superellipseInit
4160
+ * @function
4161
+ * @param {Object} [options] - Опции инициализации.
4162
+ * @param {boolean} [options.force] - Если true, пересоздаёт контроллер, даже если он уже существует.
4163
+ * @param {string} [options.mode='svg-layer'] - Режим работы ('svg-layer' или 'clip-path').
4164
+ * @param {number} [options.curveFactor] - Коэффициент кривизны углов (диапазон -2..2).
4165
+ * @param {number} [options.precision=2] - Количество знаков после запятой в генерируемом пути.
4166
+ * @returns {SuperellipseController} Контроллер, связанный с элементом.
4167
+ */
4168
+ Element.prototype.superellipseInit = function(options) {
4169
+ let controller = jsse_controllers.get(this);
4170
+
4171
+ if (controller && !options?.force) {
4172
+ jsse_console.warn({label:'API', element: this}, 'The element already has a controller. Use {force:true} to recreate it.');
4173
+ return controller;
4174
+ }
4175
+
4176
+ if (controller) {
4177
+ controller.destroy();
4178
+ }
4179
+ controller = new SuperellipseController(this, options);
4180
+ jsse_controllers.set(this, controller);
4181
+ return controller; // для цепочек
4182
+ };
4183
+
4184
+
4185
+ /**
4186
+ * Инициализирует один или несколько элементов суперэллипсом.
4187
+ * @function superellipseInit
4188
+ * @param {string|Element|NodeList|Array<Element>} target - CSS-селектор, DOM-элемент или коллекция элементов.
4189
+ * @param {Object} [options] - Опции инициализации (см. Element.prototype.superellipseInit).
4190
+ * @returns {SuperellipseController|Array<SuperellipseController>} Контроллер для одного элемента или массив контроллеров для нескольких.
4191
+ * @throws {Error} Если первый аргумент не является селектором, элементом или коллекцией.
4192
+ */
4193
+ function superellipseInit(target, options) {
4194
+ if (typeof target === 'string') {
4195
+ const elements = document.querySelectorAll(target);
4196
+ const controllersList = [];
4197
+ elements.forEach(el => {
4198
+ el.superellipseInit(options);
4199
+ controllersList.push(el.superellipse);
4200
+ });
4201
+ return controllersList;
4202
+ } else if (target instanceof Element) {
4203
+ target.superellipseInit(options);
4204
+ return target.superellipse;
4205
+ } else if (target instanceof NodeList || Array.isArray(target)) {
4206
+ const controllersList = [];
4207
+ for (let i = 0; i < target.length; i++) {
4208
+ const el = target[i];
4209
+ if (el instanceof Element) {
4210
+ el.superellipseInit(options);
4211
+ controllersList.push(el.superellipse);
4212
+ }
4213
+ }
4214
+ return controllersList;
4215
+ } else {
4216
+ throw new Error('superellipseInit: первый аргумент должен быть селектором, элементом или коллекцией элементов');
4217
+ }
4218
+ }
4219
+ if (typeof window !== 'undefined') {
4220
+ window.superellipseInit = superellipseInit;
4221
+ }
4222
+
4223
+ exports.SuperellipseController = SuperellipseController;
4224
+ exports.jsse_generateSuperellipsePath = jsse_generateSuperellipsePath;
4225
+ exports.superellipseInit = superellipseInit;
4226
+
4227
+ }));
4228
+ //# sourceMappingURL=superellipse.js.map