@nemigo/helpers 0.13.1 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/colors.js ADDED
@@ -0,0 +1,888 @@
1
+ import { clamp, toRound, toZero } from "./phymath/index.js";
2
+ //...
3
+ /**
4
+ * Ограничивает значение RGB-канала в диапазоне 0-255
5
+ */
6
+ export const clampRgb = (value) => clamp(Math.round(value), 0, 255);
7
+ /**
8
+ * Ограничивает оттенок в диапазоне 0-360
9
+ */
10
+ export const clampHue = (value) => {
11
+ const v = value % 360;
12
+ return toZero(v < 0 ? v + 360 : v);
13
+ };
14
+ /**
15
+ * Ограничивает процентное значение в диапазоне 0-100
16
+ */
17
+ export const clampPercent = (value) => clamp(value, 0, 100);
18
+ /**
19
+ * Ограничивает значение альфа-канала в диапазоне 0-1
20
+ */
21
+ export const clampAlpha = (value) => clamp(value, 0, 1);
22
+ //...
23
+ /**
24
+ * Проверяет, является ли объект RGB
25
+ */
26
+ const isRGB = (color) => typeof color === "object" && color !== null && "r" in color && "g" in color && "b" in color;
27
+ /**
28
+ * Проверяет, является ли объект RGBA
29
+ */
30
+ const isRGBA = (color) => isRGB(color) && "a" in color;
31
+ /**
32
+ * Проверяет, является ли объект HSL
33
+ */
34
+ const isHSL = (color) => typeof color === "object" && color !== null && "h" in color && "s" in color && "l" in color;
35
+ /**
36
+ * Проверяет, является ли объект HSLA
37
+ */
38
+ const isHSLA = (color) => isHSL(color) && "a" in color;
39
+ /**
40
+ * Проверяет, является ли объект HSV
41
+ */
42
+ const isHSV = (color) => typeof color === "object" && color !== null && "h" in color && "s" in color && "v" in color;
43
+ /**
44
+ * Проверяет, является ли объект HSVA
45
+ */
46
+ const isHSVA = (color) => isHSV(color) && "a" in color;
47
+ //...
48
+ /**
49
+ * Парсит HEX-строку в RGB или RGBA
50
+ *
51
+ * Поддерживаемые форматы:
52
+ * - #RGB
53
+ * - #RRGGBB
54
+ * - #RGBA
55
+ * - #RRGGBBAA
56
+ *
57
+ * @param hex - HEX-строка цвета
58
+ * @returns RGB или RGBA объект, или null если формат неверен
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * parseHex("#ff0000"); // { r: 255, g: 0, b: 0 }
63
+ * parseHex("#f00"); // { r: 255, g: 0, b: 0 }
64
+ * parseHex("#ff0000ff"); // { r: 255, g: 0, b: 0, a: 1 }
65
+ * parseHex("#f00f"); // { r: 255, g: 0, b: 0, a: 1 }
66
+ * parseHex("invalid"); // null
67
+ * ```
68
+ */
69
+ export const parseHex = (hex) => {
70
+ const cleaned = hex.trim().replace(/^#/, "");
71
+ // #RGB или #RGBA
72
+ if (cleaned.length === 3 || cleaned.length === 4) {
73
+ const r = parseInt(cleaned[0] + cleaned[0], 16);
74
+ const g = parseInt(cleaned[1] + cleaned[1], 16);
75
+ const b = parseInt(cleaned[2] + cleaned[2], 16);
76
+ if (isNaN(r) || isNaN(g) || isNaN(b))
77
+ return null;
78
+ if (cleaned.length === 4) {
79
+ const a = parseInt(cleaned[3] + cleaned[3], 16) / 255;
80
+ if (isNaN(a))
81
+ return null;
82
+ return { r, g, b, a: toRound(a, 3) };
83
+ }
84
+ return { r, g, b };
85
+ }
86
+ // #RRGGBB или #RRGGBBAA
87
+ if (cleaned.length === 6 || cleaned.length === 8) {
88
+ const r = parseInt(cleaned.substring(0, 2), 16);
89
+ const g = parseInt(cleaned.substring(2, 4), 16);
90
+ const b = parseInt(cleaned.substring(4, 6), 16);
91
+ if (isNaN(r) || isNaN(g) || isNaN(b))
92
+ return null;
93
+ if (cleaned.length === 8) {
94
+ const a = parseInt(cleaned.substring(6, 8), 16) / 255;
95
+ if (isNaN(a))
96
+ return null;
97
+ return { r, g, b, a: toRound(a, 3) };
98
+ }
99
+ return { r, g, b };
100
+ }
101
+ return null;
102
+ };
103
+ /**
104
+ * Парсит RGB/RGBA строку в объект
105
+ *
106
+ * Поддерживаемые форматы:
107
+ * - rgb(r, g, b)
108
+ * - rgba(r, g, b, a)
109
+ * - rgb(r g b)
110
+ * - rgba(r g b / a)
111
+ *
112
+ * @param rgb - RGB/RGBA строка
113
+ * @returns RGB или RGBA объект, или null если формат неверен
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * parseRgb("rgb(255, 0, 0)"); // { r: 255, g: 0, b: 0 }
118
+ * parseRgb("rgba(255, 0, 0, 0.5)"); // { r: 255, g: 0, b: 0, a: 0.5 }
119
+ * parseRgb("rgb(255 0 0)"); // { r: 255, g: 0, b: 0 }
120
+ * parseRgb("rgba(255 0 0 / 0.5)"); // { r: 255, g: 0, b: 0, a: 0.5 }
121
+ * ```
122
+ */
123
+ export const parseRgb = (rgb) => {
124
+ const match = rgb.trim().match(/^rgba?\(\s*([^)]+)\s*\)$/i);
125
+ if (!match)
126
+ return null;
127
+ const parts = match[1]
128
+ .split(/[\s,/]+/)
129
+ .map((v) => v.trim())
130
+ .filter((v) => v !== "");
131
+ if (parts.length < 3 || parts.length > 4)
132
+ return null;
133
+ const r = parseInt(parts[0], 10);
134
+ const g = parseInt(parts[1], 10);
135
+ const b = parseInt(parts[2], 10);
136
+ if (isNaN(r) || isNaN(g) || isNaN(b))
137
+ return null;
138
+ if (parts.length === 4) {
139
+ const a = parseFloat(parts[3]);
140
+ if (isNaN(a))
141
+ return null;
142
+ return { r: clampRgb(r), g: clampRgb(g), b: clampRgb(b), a: clampAlpha(a) };
143
+ }
144
+ return { r: clampRgb(r), g: clampRgb(g), b: clampRgb(b) };
145
+ };
146
+ /**
147
+ * Парсит HSL/HSLA строку в объект
148
+ *
149
+ * Поддерживаемые форматы:
150
+ * - hsl(h, s%, l%)
151
+ * - hsla(h, s%, l%, a)
152
+ * - hsl(h s% l%)
153
+ * - hsla(h s% l% / a)
154
+ *
155
+ * @param hsl - HSL/HSLA строка
156
+ * @returns HSL или HSLA объект, или null если формат неверен
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * parseHsl("hsl(0, 100%, 50%)"); // { h: 0, s: 100, l: 50 }
161
+ * parseHsl("hsla(0, 100%, 50%, 0.5)"); // { h: 0, s: 100, l: 50, a: 0.5 }
162
+ * parseHsl("hsl(0 100% 50%)"); // { h: 0, s: 100, l: 50 }
163
+ * ```
164
+ */
165
+ export const parseHsl = (hsl) => {
166
+ const match = hsl.trim().match(/^hsla?\(\s*([^)]+)\s*\)$/i);
167
+ if (!match)
168
+ return null;
169
+ const parts = match[1]
170
+ .split(/[\s,/]+/)
171
+ .map((v) => v.trim())
172
+ .filter((v) => v !== "");
173
+ if (parts.length < 3 || parts.length > 4)
174
+ return null;
175
+ const h = parseFloat(parts[0]);
176
+ const s = parseFloat(parts[1].replace("%", ""));
177
+ const l = parseFloat(parts[2].replace("%", ""));
178
+ if (isNaN(h) || isNaN(s) || isNaN(l))
179
+ return null;
180
+ if (parts.length === 4) {
181
+ const a = parseFloat(parts[3]);
182
+ if (isNaN(a))
183
+ return null;
184
+ return { h: clampHue(h), s: clampPercent(s), l: clampPercent(l), a: clampAlpha(a) };
185
+ }
186
+ return { h: clampHue(h), s: clampPercent(s), l: clampPercent(l) };
187
+ };
188
+ /**
189
+ * Универсальный парсер цвета
190
+ *
191
+ * Пробует распознать формат и парсит соответствующим парсером
192
+ *
193
+ * @param color - Строка цвета в любом поддерживаемом формате
194
+ * @returns RGB или RGBA объект, или null если формат не распознан
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * parseColor("#ff0000"); // { r: 255, g: 0, b: 0 }
199
+ * parseColor("rgb(255, 0, 0)"); // { r: 255, g: 0, b: 0 }
200
+ * parseColor("hsl(0, 100%, 50%)"); // { r: 255, g: 0, b: 0 }
201
+ * ```
202
+ */
203
+ export const parseColor = (color) => {
204
+ const trimmed = color.trim();
205
+ // Пробуем HEX
206
+ if (trimmed.startsWith("#")) {
207
+ return parseHex(trimmed);
208
+ }
209
+ // Пробуем RGB/RGBA
210
+ if (trimmed.startsWith("rgb")) {
211
+ return parseRgb(trimmed);
212
+ }
213
+ // Пробуем HSL/HSLA (конвертируем в RGB)
214
+ if (trimmed.startsWith("hsl")) {
215
+ const hsl = parseHsl(trimmed);
216
+ if (hsl)
217
+ return hslToRgb(hsl);
218
+ }
219
+ return null;
220
+ };
221
+ //...
222
+ /**
223
+ * Конвертирует RGB/RGBA в HEX/HEXA строку
224
+ *
225
+ * @param rgb - RGB или RGBA объект
226
+ * @returns HEX строка в формате #RRGGBB или #RRGGBBAA
227
+ *
228
+ * @example
229
+ * ```typescript
230
+ * rgbToHex({ r: 255, g: 0, b: 0 }); // "#ff0000"
231
+ * rgbToHex({ r: 255, g: 0, b: 0, a: 0.5 }); // "#ff000080"
232
+ * ```
233
+ */
234
+ export const rgbToHex = (rgb) => {
235
+ const r = clampRgb(rgb.r).toString(16).padStart(2, "0");
236
+ const g = clampRgb(rgb.g).toString(16).padStart(2, "0");
237
+ const b = clampRgb(rgb.b).toString(16).padStart(2, "0");
238
+ if (isRGBA(rgb)) {
239
+ const a = Math.round(clampAlpha(rgb.a) * 255)
240
+ .toString(16)
241
+ .padStart(2, "0");
242
+ return `#${r}${g}${b}${a}`;
243
+ }
244
+ return `#${r}${g}${b}`;
245
+ };
246
+ /**
247
+ * Конвертирует HEX строку в RGB/RGBA объект
248
+ *
249
+ * @param hex - HEX строка
250
+ * @returns RGB или RGBA объект
251
+ * @throws {Error} Если формат HEX неверен
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * hexToRgb("#ff0000"); // { r: 255, g: 0, b: 0 }
256
+ * hexToRgb("#f00"); // { r: 255, g: 0, b: 0 }
257
+ * ```
258
+ */
259
+ export const hexToRgb = (hex) => {
260
+ const result = parseHex(hex);
261
+ if (!result)
262
+ throw new Error(`Неверный HEX формат: ${hex}`);
263
+ return result;
264
+ };
265
+ /**
266
+ * Конвертирует RGB/RGBA в HSL/HSLA
267
+ *
268
+ * @param rgb - RGB или RGBA объект
269
+ * @returns HSL или HSLA объект
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * rgbToHsl({ r: 255, g: 0, b: 0 }); // { h: 0, s: 100, l: 50 }
274
+ * rgbToHsl({ r: 255, g: 255, b: 255 }); // { h: 0, s: 0, l: 100 }
275
+ * ```
276
+ */
277
+ export const rgbToHsl = (rgb) => {
278
+ const r = rgb.r / 255;
279
+ const g = rgb.g / 255;
280
+ const b = rgb.b / 255;
281
+ const max = Math.max(r, g, b);
282
+ const min = Math.min(r, g, b);
283
+ const delta = max - min;
284
+ let h = 0;
285
+ let s = 0;
286
+ const l = (max + min) / 2;
287
+ if (delta !== 0) {
288
+ s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
289
+ switch (max) {
290
+ case r:
291
+ h = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
292
+ break;
293
+ case g:
294
+ h = ((b - r) / delta + 2) / 6;
295
+ break;
296
+ case b:
297
+ h = ((r - g) / delta + 4) / 6;
298
+ break;
299
+ }
300
+ }
301
+ const result = {
302
+ h: toRound(h * 360, 1),
303
+ s: toRound(s * 100, 1),
304
+ l: toRound(l * 100, 1),
305
+ };
306
+ if (isRGBA(rgb)) {
307
+ return { ...result, a: rgb.a };
308
+ }
309
+ return result;
310
+ };
311
+ /**
312
+ * Конвертирует HSL/HSLA в RGB/RGBA
313
+ *
314
+ * @param hsl - HSL или HSLA объект
315
+ * @returns RGB или RGBA объект
316
+ *
317
+ * @example
318
+ * ```typescript
319
+ * hslToRgb({ h: 0, s: 100, l: 50 }); // { r: 255, g: 0, b: 0 }
320
+ * hslToRgb({ h: 0, s: 0, l: 100 }); // { r: 255, g: 255, b: 255 }
321
+ * ```
322
+ */
323
+ export const hslToRgb = (hsl) => {
324
+ const h = hsl.h / 360;
325
+ const s = hsl.s / 100;
326
+ const l = hsl.l / 100;
327
+ let r, g, b;
328
+ if (s === 0) {
329
+ r = g = b = l;
330
+ }
331
+ else {
332
+ const hue2rgb = (p, q, t) => {
333
+ if (t < 0)
334
+ t += 1;
335
+ if (t > 1)
336
+ t -= 1;
337
+ if (t < 1 / 6)
338
+ return p + (q - p) * 6 * t;
339
+ if (t < 1 / 2)
340
+ return q;
341
+ if (t < 2 / 3)
342
+ return p + (q - p) * (2 / 3 - t) * 6;
343
+ return p;
344
+ };
345
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
346
+ const p = 2 * l - q;
347
+ r = hue2rgb(p, q, h + 1 / 3);
348
+ g = hue2rgb(p, q, h);
349
+ b = hue2rgb(p, q, h - 1 / 3);
350
+ }
351
+ const result = {
352
+ r: Math.round(r * 255),
353
+ g: Math.round(g * 255),
354
+ b: Math.round(b * 255),
355
+ };
356
+ if (isHSLA(hsl)) {
357
+ return { ...result, a: hsl.a };
358
+ }
359
+ return result;
360
+ };
361
+ /**
362
+ * Конвертирует RGB/RGBA в HSV/HSVA
363
+ *
364
+ * @param rgb - RGB или RGBA объект
365
+ * @returns HSV или HSVA объект
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * rgbToHsv({ r: 255, g: 0, b: 0 }); // { h: 0, s: 100, v: 100 }
370
+ * rgbToHsv({ r: 0, g: 0, b: 0 }); // { h: 0, s: 0, v: 0 }
371
+ * ```
372
+ */
373
+ export const rgbToHsv = (rgb) => {
374
+ const r = rgb.r / 255;
375
+ const g = rgb.g / 255;
376
+ const b = rgb.b / 255;
377
+ const max = Math.max(r, g, b);
378
+ const min = Math.min(r, g, b);
379
+ const delta = max - min;
380
+ let h = 0;
381
+ let s = 0;
382
+ const v = max;
383
+ if (delta !== 0) {
384
+ s = delta / max;
385
+ switch (max) {
386
+ case r:
387
+ h = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
388
+ break;
389
+ case g:
390
+ h = ((b - r) / delta + 2) / 6;
391
+ break;
392
+ case b:
393
+ h = ((r - g) / delta + 4) / 6;
394
+ break;
395
+ }
396
+ }
397
+ const result = {
398
+ h: toRound(h * 360, 1),
399
+ s: toRound(s * 100, 1),
400
+ v: toRound(v * 100, 1),
401
+ };
402
+ if (isRGBA(rgb)) {
403
+ return { ...result, a: rgb.a };
404
+ }
405
+ return result;
406
+ };
407
+ /**
408
+ * Конвертирует HSV/HSVA в RGB/RGBA
409
+ *
410
+ * @param hsv - HSV или HSVA объект
411
+ * @returns RGB или RGBA объект
412
+ *
413
+ * @example
414
+ * ```typescript
415
+ * hsvToRgb({ h: 0, s: 100, v: 100 }); // { r: 255, g: 0, b: 0 }
416
+ * hsvToRgb({ h: 0, s: 0, v: 0 }); // { r: 0, g: 0, b: 0 }
417
+ * ```
418
+ */
419
+ export const hsvToRgb = (hsv) => {
420
+ const h = hsv.h / 360;
421
+ const s = hsv.s / 100;
422
+ const v = hsv.v / 100;
423
+ const i = Math.floor(h * 6);
424
+ const f = h * 6 - i;
425
+ const p = v * (1 - s);
426
+ const q = v * (1 - f * s);
427
+ const t = v * (1 - (1 - f) * s);
428
+ let r, g, b;
429
+ switch (i % 6) {
430
+ case 0:
431
+ r = v;
432
+ g = t;
433
+ b = p;
434
+ break;
435
+ case 1:
436
+ r = q;
437
+ g = v;
438
+ b = p;
439
+ break;
440
+ case 2:
441
+ r = p;
442
+ g = v;
443
+ b = t;
444
+ break;
445
+ case 3:
446
+ r = p;
447
+ g = q;
448
+ b = v;
449
+ break;
450
+ case 4:
451
+ r = t;
452
+ g = p;
453
+ b = v;
454
+ break;
455
+ case 5:
456
+ default:
457
+ r = v;
458
+ g = p;
459
+ b = q;
460
+ break;
461
+ }
462
+ const result = {
463
+ r: Math.round(r * 255),
464
+ g: Math.round(g * 255),
465
+ b: Math.round(b * 255),
466
+ };
467
+ if (isHSVA(hsv)) {
468
+ return { ...result, a: hsv.a };
469
+ }
470
+ return result;
471
+ };
472
+ //...
473
+ /**
474
+ * Форматирует RGB/RGBA в CSS-строку
475
+ *
476
+ * @param rgb - RGB или RGBA объект
477
+ * @returns CSS-строка в формате rgb(r, g, b) или rgba(r, g, b, a)
478
+ *
479
+ * @example
480
+ * ```typescript
481
+ * toRgbString({ r: 255, g: 0, b: 0 }); // "rgb(255, 0, 0)"
482
+ * toRgbString({ r: 255, g: 0, b: 0, a: 0.5 }); // "rgba(255, 0, 0, 0.5)"
483
+ * ```
484
+ */
485
+ export const toRgbString = (rgb) => {
486
+ if (isRGBA(rgb)) {
487
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${toRound(rgb.a, 3)})`;
488
+ }
489
+ return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
490
+ };
491
+ /**
492
+ * Форматирует RGB/RGBA в HEX-строку
493
+ *
494
+ * @param rgb - RGB или RGBA объект
495
+ * @returns HEX-строка в формате #RRGGBB или #RRGGBBAA
496
+ *
497
+ * @example
498
+ * ```typescript
499
+ * toHexString({ r: 255, g: 0, b: 0 }); // "#ff0000"
500
+ * toHexString({ r: 255, g: 0, b: 0, a: 0.5 }); // "#ff000080"
501
+ * ```
502
+ */
503
+ export const toHexString = (rgb) => rgbToHex(rgb);
504
+ /**
505
+ * Форматирует HSL/HSLA в CSS-строку
506
+ *
507
+ * @param hsl - HSL или HSLA объект
508
+ * @returns CSS-строка в формате hsl(h, s%, l%) или hsla(h, s%, l%, a)
509
+ *
510
+ * @example
511
+ * ```typescript
512
+ * toHslString({ h: 0, s: 100, l: 50 }); // "hsl(0, 100%, 50%)"
513
+ * toHslString({ h: 0, s: 100, l: 50, a: 0.5 }); // "hsla(0, 100%, 50%, 0.5)"
514
+ * ```
515
+ */
516
+ export const toHslString = (hsl) => {
517
+ if (isHSLA(hsl)) {
518
+ return `hsla(${toRound(hsl.h, 1)}, ${toRound(hsl.s, 1)}%, ${toRound(hsl.l, 1)}%, ${toRound(hsl.a, 3)})`;
519
+ }
520
+ return `hsl(${toRound(hsl.h, 1)}, ${toRound(hsl.s, 1)}%, ${toRound(hsl.l, 1)}%)`;
521
+ };
522
+ //...
523
+ /**
524
+ * Вычисляет относительную яркость цвета по формуле WCAG
525
+ *
526
+ * @param rgb - RGB объект
527
+ * @returns Относительная яркость (0-1)
528
+ *
529
+ * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
530
+ *
531
+ * @example
532
+ * ```typescript
533
+ * getLuminance({ r: 255, g: 255, b: 255 }); // 1 (белый)
534
+ * getLuminance({ r: 0, g: 0, b: 0 }); // 0 (чёрный)
535
+ * getLuminance({ r: 255, g: 0, b: 0 }); // 0.2126 (красный)
536
+ * ```
537
+ */
538
+ export const getLuminance = (rgb) => {
539
+ const rsRGB = rgb.r / 255;
540
+ const gsRGB = rgb.g / 255;
541
+ const bsRGB = rgb.b / 255;
542
+ const r = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
543
+ const g = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
544
+ const b = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
545
+ return toRound(0.2126 * r + 0.7152 * g + 0.0722 * b, 4);
546
+ };
547
+ /**
548
+ * Вычисляет контрастность между двумя цветами по формуле WCAG
549
+ *
550
+ * @param rgb1 - Первый RGB объект
551
+ * @param rgb2 - Второй RGB объект
552
+ * @returns Коэффициент контрастности (1-21)
553
+ *
554
+ * @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef
555
+ *
556
+ * @example
557
+ * ```typescript
558
+ * getContrast({ r: 255, g: 255, b: 255 }, { r: 0, g: 0, b: 0 }); // 21 (максимальный контраст)
559
+ * getContrast({ r: 255, g: 255, b: 255 }, { r: 255, g: 255, b: 255 }); // 1 (нет контраста)
560
+ * ```
561
+ */
562
+ export const getContrast = (rgb1, rgb2) => {
563
+ const lum1 = getLuminance(rgb1);
564
+ const lum2 = getLuminance(rgb2);
565
+ const lighter = Math.max(lum1, lum2);
566
+ const darker = Math.min(lum1, lum2);
567
+ return toRound((lighter + 0.05) / (darker + 0.05), 2);
568
+ };
569
+ //...
570
+ /**
571
+ * Класс для работы с цветами
572
+ *
573
+ * Поддерживает конвертацию между форматами, манипуляции с цветом и вычисление характеристик
574
+ *
575
+ * @example
576
+ * ```typescript
577
+ * const color = new Color("#ff0000");
578
+ * color.rgb; // { r: 255, g: 0, b: 0 }
579
+ * color.hex; // "#ff0000"
580
+ * color.lighten(20).hex; // "#ff6666"
581
+ * color.luminance(); // 0.2126
582
+ * ```
583
+ */
584
+ export class Color {
585
+ _rgb;
586
+ /**
587
+ * Создаёт экземпляр Color
588
+ *
589
+ * @param color - Строка цвета или RGB/RGBA объект
590
+ * @throws {Error} Если формат цвета не распознан
591
+ *
592
+ * @example
593
+ * ```typescript
594
+ * new Color("#ff0000");
595
+ * new Color("rgb(255, 0, 0)");
596
+ * new Color({ r: 255, g: 0, b: 0 });
597
+ * new Color({ r: 255, g: 0, b: 0, a: 0.5 });
598
+ * ```
599
+ */
600
+ constructor(color) {
601
+ if (typeof color === "string") {
602
+ const parsed = parseColor(color);
603
+ if (!parsed)
604
+ throw new Error(`Не удалось распознать цвет: ${color}`);
605
+ this._rgb = isRGBA(parsed) ? parsed : { ...parsed, a: 1 };
606
+ }
607
+ else if (isRGBA(color)) {
608
+ this._rgb = {
609
+ r: clampRgb(color.r),
610
+ g: clampRgb(color.g),
611
+ b: clampRgb(color.b),
612
+ a: clampAlpha(color.a),
613
+ };
614
+ }
615
+ else {
616
+ this._rgb = {
617
+ r: clampRgb(color.r),
618
+ g: clampRgb(color.g),
619
+ b: clampRgb(color.b),
620
+ a: 1,
621
+ };
622
+ }
623
+ }
624
+ /**
625
+ * Получает RGB представление цвета
626
+ */
627
+ get rgb() {
628
+ return { r: this._rgb.r, g: this._rgb.g, b: this._rgb.b };
629
+ }
630
+ /**
631
+ * Получает RGBA представление цвета
632
+ */
633
+ get rgba() {
634
+ return { ...this._rgb };
635
+ }
636
+ /**
637
+ * Получает HSL представление цвета
638
+ */
639
+ get hsl() {
640
+ const result = rgbToHsl(this._rgb);
641
+ if (isHSLA(result)) {
642
+ const { a, ...hsl } = result;
643
+ return hsl;
644
+ }
645
+ return result;
646
+ }
647
+ /**
648
+ * Получает HSLA представление цвета
649
+ */
650
+ get hsla() {
651
+ const result = rgbToHsl(this._rgb);
652
+ if (isHSLA(result))
653
+ return result;
654
+ return { ...result, a: this._rgb.a };
655
+ }
656
+ /**
657
+ * Получает HSV представление цвета
658
+ */
659
+ get hsv() {
660
+ const result = rgbToHsv(this._rgb);
661
+ if (isHSVA(result)) {
662
+ const { a, ...hsv } = result;
663
+ return hsv;
664
+ }
665
+ return result;
666
+ }
667
+ /**
668
+ * Получает HSVA представление цвета
669
+ */
670
+ get hsva() {
671
+ const result = rgbToHsv(this._rgb);
672
+ if (isHSVA(result))
673
+ return result;
674
+ return { ...result, a: this._rgb.a };
675
+ }
676
+ /**
677
+ * Получает HEX представление цвета
678
+ */
679
+ get hex() {
680
+ if (this._rgb.a < 1) {
681
+ return rgbToHex(this._rgb);
682
+ }
683
+ return rgbToHex(this.rgb);
684
+ }
685
+ /**
686
+ * Осветляет цвет
687
+ *
688
+ * @param amount - Процент осветления (0-100)
689
+ * @returns Новый экземпляр Color
690
+ *
691
+ * @example
692
+ * ```typescript
693
+ * new Color("#ff0000").lighten(20).hex; // "#ff6666"
694
+ * ```
695
+ */
696
+ lighten(amount) {
697
+ const hsl = this.hsla;
698
+ hsl.l = clampPercent(hsl.l + amount);
699
+ return new Color(hslToRgb(hsl));
700
+ }
701
+ /**
702
+ * Затемняет цвет
703
+ *
704
+ * @param amount - Процент затемнения (0-100)
705
+ * @returns Новый экземпляр Color
706
+ *
707
+ * @example
708
+ * ```typescript
709
+ * new Color("#ff0000").darken(20).hex; // "#990000"
710
+ * ```
711
+ */
712
+ darken(amount) {
713
+ const hsl = this.hsla;
714
+ hsl.l = clampPercent(hsl.l - amount);
715
+ return new Color(hslToRgb(hsl));
716
+ }
717
+ /**
718
+ * Увеличивает насыщенность цвета
719
+ *
720
+ * @param amount - Процент увеличения насыщенности (0-100)
721
+ * @returns Новый экземпляр Color
722
+ *
723
+ * @example
724
+ * ```typescript
725
+ * new Color("#ff6666").saturate(50).hex; // "#ff1a1a"
726
+ * ```
727
+ */
728
+ saturate(amount) {
729
+ const hsl = this.hsla;
730
+ hsl.s = clampPercent(hsl.s + amount);
731
+ return new Color(hslToRgb(hsl));
732
+ }
733
+ /**
734
+ * Уменьшает насыщенность цвета
735
+ *
736
+ * @param amount - Процент уменьшения насыщенности (0-100)
737
+ * @returns Новый экземпляр Color
738
+ *
739
+ * @example
740
+ * ```typescript
741
+ * new Color("#ff0000").desaturate(50).hex; // "#bf4040"
742
+ * ```
743
+ */
744
+ desaturate(amount) {
745
+ const hsl = this.hsla;
746
+ hsl.s = clampPercent(hsl.s - amount);
747
+ return new Color(hslToRgb(hsl));
748
+ }
749
+ /**
750
+ * Устанавливает прозрачность цвета
751
+ *
752
+ * @param alpha - Значение альфа-канала (0-1)
753
+ * @returns Новый экземпляр Color
754
+ *
755
+ * @example
756
+ * ```typescript
757
+ * new Color("#ff0000").opacity(0.5).hex; // "#ff000080"
758
+ * ```
759
+ */
760
+ opacity(alpha) {
761
+ return new Color({ ...this._rgb, a: clampAlpha(alpha) });
762
+ }
763
+ /**
764
+ * Инвертирует цвет
765
+ *
766
+ * @returns Новый экземпляр Color
767
+ *
768
+ * @example
769
+ * ```typescript
770
+ * new Color("#ff0000").invert().hex; // "#00ffff"
771
+ * ```
772
+ */
773
+ invert() {
774
+ return new Color({
775
+ r: 255 - this._rgb.r,
776
+ g: 255 - this._rgb.g,
777
+ b: 255 - this._rgb.b,
778
+ a: this._rgb.a,
779
+ });
780
+ }
781
+ /**
782
+ * Смешивает цвет с другим цветом
783
+ *
784
+ * @param color - Цвет для смешивания
785
+ * @param weight - Вес первого цвета (0-1), по умолчанию 0.5
786
+ * @returns Новый экземпляр Color
787
+ *
788
+ * @example
789
+ * ```typescript
790
+ * new Color("#ff0000").mix(new Color("#0000ff")).hex; // "#800080"
791
+ * new Color("#ff0000").mix(new Color("#0000ff"), 0.75).hex; // "#bf0040"
792
+ * ```
793
+ */
794
+ mix(color, weight = 0.5) {
795
+ const w = clampAlpha(weight);
796
+ const w1 = w;
797
+ const w2 = 1 - w;
798
+ const rgba1 = this._rgb;
799
+ const rgba2 = color._rgb;
800
+ return new Color({
801
+ r: Math.round(rgba1.r * w1 + rgba2.r * w2),
802
+ g: Math.round(rgba1.g * w1 + rgba2.g * w2),
803
+ b: Math.round(rgba1.b * w1 + rgba2.b * w2),
804
+ a: rgba1.a * w1 + rgba2.a * w2,
805
+ });
806
+ }
807
+ /**
808
+ * Вычисляет относительную яркость цвета
809
+ *
810
+ * @returns Относительная яркость (0-1)
811
+ *
812
+ * @example
813
+ * ```typescript
814
+ * new Color("#ffffff").luminance(); // 1
815
+ * new Color("#000000").luminance(); // 0
816
+ * ```
817
+ */
818
+ luminance() {
819
+ return getLuminance(this.rgb);
820
+ }
821
+ /**
822
+ * Вычисляет контрастность с другим цветом
823
+ *
824
+ * @param color - Цвет для сравнения
825
+ * @returns Коэффициент контрастности (1-21)
826
+ *
827
+ * @example
828
+ * ```typescript
829
+ * new Color("#ffffff").contrast(new Color("#000000")); // 21
830
+ * new Color("#ffffff").contrast(new Color("#ffffff")); // 1
831
+ * ```
832
+ */
833
+ contrast(color) {
834
+ return getContrast(this.rgb, color.rgb);
835
+ }
836
+ /**
837
+ * Проверяет, является ли цвет светлым
838
+ *
839
+ * @returns true, если яркость > 0.5
840
+ *
841
+ * @example
842
+ * ```typescript
843
+ * new Color("#ffffff").isLight(); // true
844
+ * new Color("#000000").isLight(); // false
845
+ * ```
846
+ */
847
+ isLight() {
848
+ return this.luminance() > 0.5;
849
+ }
850
+ /**
851
+ * Проверяет, является ли цвет тёмным
852
+ *
853
+ * @returns true, если яркость <= 0.5
854
+ *
855
+ * @example
856
+ * ```typescript
857
+ * new Color("#000000").isDark(); // true
858
+ * new Color("#ffffff").isDark(); // false
859
+ * ```
860
+ */
861
+ isDark() {
862
+ return !this.isLight();
863
+ }
864
+ /**
865
+ * Преобразует цвет в строку
866
+ *
867
+ * @param format - Формат вывода (hex, rgb, hsl), по умолчанию hex
868
+ * @returns Строковое представление цвета
869
+ *
870
+ * @example
871
+ * ```typescript
872
+ * new Color("#ff0000").toString(); // "#ff0000"
873
+ * new Color("#ff0000").toString("rgb"); // "rgb(255, 0, 0)"
874
+ * new Color("#ff0000").toString("hsl"); // "hsl(0, 100%, 50%)"
875
+ * ```
876
+ */
877
+ toString(format = "hex") {
878
+ switch (format) {
879
+ case "rgb":
880
+ return toRgbString(this._rgb);
881
+ case "hsl":
882
+ return toHslString(this.hsla);
883
+ case "hex":
884
+ default:
885
+ return this.hex;
886
+ }
887
+ }
888
+ }