color-util-helpers 1.0.6 → 1.0.7

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.
Files changed (47) hide show
  1. package/ng-package.json +8 -0
  2. package/package.json +2 -15
  3. package/src/lib/assets/picture.webp +0 -0
  4. package/src/lib/color-conversion.service.spec.ts +54 -0
  5. package/src/lib/color-conversion.service.ts +35 -0
  6. package/src/lib/color-extractor.directive.spec.ts +49 -0
  7. package/src/lib/color-extractor.directive.ts +28 -0
  8. package/src/lib/color-grab.directive.ts +204 -0
  9. package/src/lib/color-lighten-darken.service.spec.ts +61 -0
  10. package/src/lib/color-lighten-darken.service.ts +83 -0
  11. package/src/lib/color-pallette.service.spec.ts +85 -0
  12. package/src/lib/color-pallette.service.ts +191 -0
  13. package/src/lib/color-scheme.service.ts +123 -0
  14. package/src/lib/color-utilities-demo/color-utilities-demo.component.css +12 -0
  15. package/src/lib/color-utilities-demo/color-utilities-demo.component.html +109 -0
  16. package/src/lib/color-utilities-demo/color-utilities-demo.component.ts +57 -0
  17. package/src/lib/color-utils.module.ts +27 -0
  18. package/src/lib/text-color.service.spec.ts +75 -0
  19. package/src/lib/text-color.service.ts +101 -0
  20. package/{public-api.d.ts → src/public-api.ts} +6 -0
  21. package/tsconfig.lib.json +32 -0
  22. package/tsconfig.lib.prod.json +10 -0
  23. package/tsconfig.spec.json +14 -0
  24. package/color-util-helpers-1.0.6.tgz +0 -0
  25. package/esm2022/color-util-helpers.mjs +0 -5
  26. package/esm2022/lib/color-conversion.service.mjs +0 -39
  27. package/esm2022/lib/color-extractor.directive.mjs +0 -37
  28. package/esm2022/lib/color-grab.directive.mjs +0 -185
  29. package/esm2022/lib/color-lighten-darken.service.mjs +0 -79
  30. package/esm2022/lib/color-pallette.service.mjs +0 -172
  31. package/esm2022/lib/color-scheme.service.mjs +0 -113
  32. package/esm2022/lib/color-utilities-demo/color-utilities-demo.component.mjs +0 -41
  33. package/esm2022/lib/color-utils.module.mjs +0 -38
  34. package/esm2022/lib/text-color.service.mjs +0 -79
  35. package/esm2022/public-api.mjs +0 -13
  36. package/fesm2022/color-util-helpers.mjs +0 -767
  37. package/fesm2022/color-util-helpers.mjs.map +0 -1
  38. package/index.d.ts +0 -5
  39. package/lib/color-conversion.service.d.ts +0 -8
  40. package/lib/color-extractor.directive.d.ts +0 -13
  41. package/lib/color-grab.directive.d.ts +0 -31
  42. package/lib/color-lighten-darken.service.d.ts +0 -10
  43. package/lib/color-pallette.service.d.ts +0 -36
  44. package/lib/color-scheme.service.d.ts +0 -45
  45. package/lib/color-utilities-demo/color-utilities-demo.component.d.ts +0 -34
  46. package/lib/color-utils.module.d.ts +0 -12
  47. package/lib/text-color.service.d.ts +0 -18
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/color-utils",
4
+ "assets": ["./assets"],
5
+ "lib": {
6
+ "entryFile": "src/public-api.ts"
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "color-util-helpers",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^15.2.0",
6
6
  "@angular/core": "^15.2.0"
@@ -8,18 +8,5 @@
8
8
  "dependencies": {
9
9
  "tslib": "^2.3.0"
10
10
  },
11
- "sideEffects": false,
12
- "module": "fesm2022/color-util-helpers.mjs",
13
- "typings": "index.d.ts",
14
- "exports": {
15
- "./package.json": {
16
- "default": "./package.json"
17
- },
18
- ".": {
19
- "types": "./index.d.ts",
20
- "esm2022": "./esm2022/color-util-helpers.mjs",
21
- "esm": "./esm2022/color-util-helpers.mjs",
22
- "default": "./fesm2022/color-util-helpers.mjs"
23
- }
24
- }
11
+ "sideEffects": false
25
12
  }
Binary file
@@ -0,0 +1,54 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { ColorConversionService } from './color-conversion.service';
3
+
4
+ describe('ColorConversionService', () => {
5
+ let service: ColorConversionService;
6
+
7
+ beforeEach(() => {
8
+ TestBed.configureTestingModule({});
9
+ service = TestBed.inject(ColorConversionService);
10
+ });
11
+
12
+ describe('rgbToHex', () => {
13
+ it('should convert RGB to hex', () => {
14
+ expect(service.rgbToHex([255, 0, 0])).toBe('#ff0000');
15
+ expect(service.rgbToHex([0, 255, 0])).toBe('#00ff00');
16
+ expect(service.rgbToHex([0, 0, 255])).toBe('#0000ff');
17
+ });
18
+
19
+ it('should return empty string for null input', () => {
20
+ expect(service.rgbToHex(null)).toBe('');
21
+ });
22
+
23
+ it('should return empty string for empty array input', () => {
24
+ expect(service.rgbToHex([])).toBe('');
25
+ });
26
+
27
+ it('should handle invalid RGB values', () => {
28
+ expect(service.rgbToHex([256, -1, 300])).toBe('');
29
+ expect(service.rgbToHex([255, 255])).toBe('');
30
+ expect(service.rgbToHex([255, 255, 255, 255])).toBe('');
31
+ });
32
+ });
33
+
34
+ describe('hexToRgb', () => {
35
+ it('should convert hex to RGB', () => {
36
+ expect(service.hexToRgb('#ff0000')).toEqual([255, 0, 0]);
37
+ expect(service.hexToRgb('#00ff00')).toEqual([0, 255, 0]);
38
+ expect(service.hexToRgb('#0000ff')).toEqual([0, 0, 255]);
39
+ });
40
+
41
+ it('should return empty array for null input', () => {
42
+ expect(service.hexToRgb(null)).toEqual([]);
43
+ });
44
+
45
+ it('should return empty array for undefined input', () => {
46
+ expect(service.hexToRgb(undefined)).toEqual([]);
47
+ });
48
+
49
+ it('should return empty array for invalid hex values', () => {
50
+ expect(service.hexToRgb('#zzz')).toEqual([]);
51
+ expect(service.hexToRgb('#12345')).toEqual([]);
52
+ });
53
+ });
54
+ });
@@ -0,0 +1,35 @@
1
+ import { Injectable } from '@angular/core';
2
+
3
+ @Injectable({
4
+ providedIn: 'root'
5
+ })
6
+ export class ColorConversionService {
7
+
8
+ rgbToHex(rgb: number[] | null): string {
9
+ if (rgb === null || rgb.length !== 3 || rgb.some(value => value < 0 || value > 255)) return '';
10
+
11
+ const [r, g, b] = rgb;
12
+ const hexR = this.componentToHex(r);
13
+ const hexG = this.componentToHex(g);
14
+ const hexB = this.componentToHex(b);
15
+ return "#" + hexR + hexG + hexB;
16
+ }
17
+
18
+ private componentToHex = (c: number) => {
19
+ const hex = c.toString(16);
20
+ return hex.length === 1 ? "0" + hex : hex;
21
+ }
22
+
23
+ hexToRgb(hex: string | null | undefined): number[] {
24
+ if (hex === null || hex === undefined) return [];
25
+
26
+ hex = (hex.length === 3) ? hex + hex : hex;
27
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
28
+
29
+ return result ? [
30
+ parseInt(result[1], 16),
31
+ parseInt(result[2], 16),
32
+ parseInt(result[3], 16)
33
+ ] : [];
34
+ }
35
+ }
@@ -0,0 +1,49 @@
1
+ import { ColorExtractorDirective } from './color-extractor.directive';
2
+ import { Component, DebugElement } from '@angular/core';
3
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
4
+ import { By } from '@angular/platform-browser';
5
+
6
+ @Component({
7
+ template: `<div getColor (colorValue)="onColorChange($event)" style="background-color: red;"></div>`
8
+ })
9
+ class TestHostComponent {
10
+
11
+ color = ''
12
+
13
+ onColorChange(color: string) {
14
+ this.color = color;
15
+ }
16
+ }
17
+
18
+ describe('ColorExtractorDirective', () => {
19
+ let fixture: ComponentFixture<TestHostComponent>;
20
+ let divEl: DebugElement;
21
+
22
+ beforeEach(() => {
23
+ TestBed.configureTestingModule({
24
+ declarations: [ColorExtractorDirective, TestHostComponent]
25
+ });
26
+
27
+ fixture = TestBed.createComponent(TestHostComponent);
28
+ divEl = fixture.debugElement.query(By.directive(ColorExtractorDirective));
29
+ fixture.detectChanges();
30
+ });
31
+
32
+ it('should create an instance', () => {
33
+ expect(divEl).toBeTruthy();
34
+ });
35
+
36
+ it('should emit background color on mouse enter', () => {
37
+ spyOn(fixture.componentInstance, 'onColorChange');
38
+ divEl.triggerEventHandler('mouseenter', null);
39
+ fixture.detectChanges();
40
+ expect(fixture.componentInstance.onColorChange).toHaveBeenCalled();
41
+ });
42
+
43
+ it('should emit white color on mouse leave', () => {
44
+ spyOn(fixture.componentInstance, 'onColorChange');
45
+ divEl.triggerEventHandler('mouseleave', null);
46
+ fixture.detectChanges();
47
+ expect(fixture.componentInstance.onColorChange).toHaveBeenCalledWith('white');
48
+ });
49
+ });
@@ -0,0 +1,28 @@
1
+ import { Directive, ElementRef, Renderer2, HostListener, EventEmitter, Output } from '@angular/core';
2
+
3
+ @Directive({
4
+ selector: '[getColor]'
5
+ })
6
+ export class ColorExtractorDirective {
7
+
8
+ @Output() colorValue: EventEmitter<string> = new EventEmitter<string>();
9
+
10
+ constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
11
+
12
+ get currentElement() {
13
+ return window.getComputedStyle(this.elementRef.nativeElement);
14
+ }
15
+
16
+ @HostListener('mouseenter')
17
+ onMouseEnter() {
18
+ // console.log('ENTER', this.currentElement)
19
+ this.colorValue.emit(this.currentElement.getPropertyValue('background-color'))
20
+ }
21
+
22
+ @HostListener('mouseleave')
23
+ onMouseLeave() {
24
+ // console.log('LEAVE', this.currentElement.getPropertyValue('background-color'))
25
+ this.colorValue.emit('white')
26
+ }
27
+
28
+ }
@@ -0,0 +1,204 @@
1
+ import { Directive, ElementRef, Input, OnInit } from '@angular/core';
2
+
3
+ @Directive({
4
+ selector: '[colorGrabber]'
5
+ })
6
+ export class ColorGrabberDirective implements OnInit {
7
+
8
+ ctx?: CanvasRenderingContext2D
9
+
10
+ @Input() imageUrl?: string
11
+ @Input() light?: string
12
+ @Input() dark?: string
13
+
14
+ constructor(private el: ElementRef) { }
15
+
16
+ ngOnInit() {
17
+
18
+ const canvas = document.createElement('canvas');
19
+ canvas.width = 1;
20
+ canvas.height = 1;
21
+
22
+ this.ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
23
+
24
+ const img = new Image();
25
+ img.src = this.imageUrl || '';
26
+ img.setAttribute('crossOrigin', '');
27
+
28
+ img.onload = () => {
29
+ this.ctx?.drawImage(img, 0, 0, 1, 1);
30
+ const imageData = this.ctx?.getImageData(0, 0, 1, 1);
31
+
32
+ if (imageData && imageData.data) {
33
+ const i = imageData.data;
34
+
35
+ const rgbColor = `rgba(${i[0]},${i[1]},${i[2]},${i[3]})`;
36
+ const hexColor = "#" + ((1 << 24) + (i[0] << 16) + (i[1] << 8) + i[2]).toString(16).slice(1);
37
+
38
+ const textColor = this.textColorBasedOnBgColor(hexColor, this.light, this.dark);
39
+
40
+ const hsv = this.RGB2HSV({ r: i[0], g: i[1], b: i[2] });
41
+ hsv.hue = this.HueShift(hsv.hue, 135.0);
42
+
43
+ const secondaryColor = this.HSV2RGB(hsv);
44
+ const highlightColor = this.lightenDarkenColor(secondaryColor.hex, 50);
45
+
46
+ this.el.nativeElement.style.backgroundColor = rgbColor;
47
+ this.el.nativeElement.style.color = textColor;
48
+ } else {
49
+ console.error("Failed to get image data.");
50
+ }
51
+ };
52
+ }
53
+
54
+ textColorBasedOnBgColor(
55
+ bgColor: string,
56
+ lightColor: string = '#FFFFFF',
57
+ darkColor: string = '#000000'
58
+ ) {
59
+
60
+ const color = (bgColor.charAt(0) === '#') ? bgColor.substring(1, 7) : bgColor
61
+
62
+ const r = parseInt(color.substring(0, 2), 16) // hexToR
63
+ const g = parseInt(color.substring(2, 4), 16) // hexToG
64
+ const b = parseInt(color.substring(4, 6), 16) // hexToB
65
+
66
+ const uicolors = [r / 255, g / 255, b / 255]
67
+
68
+ const c = uicolors.map((col) => {
69
+
70
+ if (col <= 0.03928) return col / 12.92
71
+ return Math.pow((col + 0.055) / 1.055, 2.4)
72
+
73
+ })
74
+
75
+ const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2])
76
+
77
+ return (L > 0.179) ? darkColor : lightColor
78
+
79
+ }
80
+
81
+ RGB2HSV(rgb: { r: any, g: any, b: any}) {
82
+
83
+ const hsv = { saturation:0, hue:0, value: 0 }
84
+
85
+ const max = this.max3(rgb.r,rgb.g,rgb.b)
86
+ const dif = max - this.min3(rgb.r,rgb.g,rgb.b)
87
+ hsv.saturation = (max==0.0)?0:(100*dif/max)
88
+
89
+ if (hsv.saturation == 0) {
90
+ hsv.hue=0
91
+ } else if (rgb.r == max) {
92
+ hsv.hue=60.0*(rgb.g-rgb.b)/dif
93
+ } else if (rgb.g == max) {
94
+ hsv.hue=120.0+60.0*(rgb.b-rgb.r)/dif
95
+ } else if (rgb.b == max) {
96
+ hsv.hue=240.0+60.0*(rgb.r-rgb.g)/dif
97
+ }
98
+
99
+ if (hsv.hue<0.0) hsv.hue+=360.0
100
+
101
+ hsv.value = Math.round(max*100/255)
102
+ hsv.hue = Math.round(hsv.hue)
103
+ hsv.saturation = Math.round(hsv.saturation)
104
+
105
+ return hsv
106
+ }
107
+
108
+ HSV2RGB(hsv: any) {
109
+
110
+ const rgb = { r: 0, g: 0, b: 0}
111
+
112
+ if (hsv.saturation==0) {
113
+ rgb.r = rgb.g = rgb.b = Math.round(hsv.value*2.55)
114
+ } else {
115
+
116
+ hsv.hue/=60
117
+ hsv.saturation/=100
118
+ hsv.value/=100
119
+ const i = Math.floor(hsv.hue)
120
+ const f = hsv.hue-i
121
+
122
+ const p = hsv.value*(1-hsv.saturation)
123
+ const q = hsv.value*(1-hsv.saturation*f)
124
+ const t = hsv.value*(1-hsv.saturation*(1-f))
125
+
126
+ switch(i) {
127
+
128
+ case 0: rgb.r=hsv.value; rgb.g=t; rgb.b=p; break
129
+ case 1: rgb.r=q; rgb.g=hsv.value; rgb.b=p; break
130
+ case 2: rgb.r=p; rgb.g=hsv.value; rgb.b=t; break
131
+ case 3: rgb.r=p; rgb.g=q; rgb.b=hsv.value; break
132
+ case 4: rgb.r=t; rgb.g=p; rgb.b=hsv.value; break
133
+
134
+ default: rgb.r=hsv.value; rgb.g=p; rgb.b=q
135
+
136
+ }
137
+
138
+ rgb.r = Math.round(rgb.r*255)
139
+ rgb.g = Math.round(rgb.g*255)
140
+ rgb.b = Math.round(rgb.b*255)
141
+
142
+ }
143
+
144
+ const rgbColor = `rgba(${rgb.r},${rgb.g},${rgb.b},${1})`
145
+ const hexColor = "#" + ((1 << 24) + (rgb.r << 16) + (rgb.g << 8) + rgb.b).toString(16).slice(1)
146
+
147
+ return { rgb: rgbColor, hex: hexColor }
148
+ }
149
+
150
+ HueShift(h: any, s: any) {
151
+ h += s
152
+ while (h>=360.0) h-=360.0
153
+ while (h<0.0) h+=360.0
154
+ return h
155
+ }
156
+
157
+ min3(a: any, b: any, c: any) {
158
+ return (a<b)?((a<c)?a:c):((b<c)?b:c)
159
+ }
160
+
161
+ max3(a: any, b: any, c: any) {
162
+ return (a>b)?((a>c)?a:c):((b>c)?b:c)
163
+ }
164
+
165
+ lightenDarkenColor(colorCode: any, amount: number) {
166
+
167
+ var usePound = false;
168
+
169
+ if (colorCode[0] == "#") {
170
+ colorCode = colorCode.slice(1);
171
+ usePound = true;
172
+ }
173
+
174
+ var num = parseInt(colorCode, 16);
175
+
176
+ var r = (num >> 16) + amount;
177
+
178
+ if (r > 255) {
179
+ r = 255;
180
+ } else if (r < 0) {
181
+ r = 0;
182
+ }
183
+
184
+ var b = ((num >> 8) & 0x00FF) + amount;
185
+
186
+ if (b > 255) {
187
+ b = 255;
188
+ } else if (b < 0) {
189
+ b = 0;
190
+ }
191
+
192
+ var g = (num & 0x0000FF) + amount;
193
+
194
+ if (g > 255) {
195
+ g = 255;
196
+ } else if (g < 0) {
197
+ g = 0;
198
+ }
199
+
200
+ return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16)
201
+
202
+ }
203
+
204
+ }
@@ -0,0 +1,61 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { ColorLightenDarkenService } from './color-lighten-darken.service';
3
+ import { TextColorService } from './text-color.service';
4
+
5
+ describe('ColorLightenDarkenService', () => {
6
+ let service: ColorLightenDarkenService;
7
+ let textColorServiceSpy: jasmine.SpyObj<TextColorService>;
8
+
9
+ beforeEach(() => {
10
+ const spy = jasmine.createSpyObj('TextColorService', ['fixColor']);
11
+
12
+ TestBed.configureTestingModule({
13
+ providers: [
14
+ ColorLightenDarkenService,
15
+ { provide: TextColorService, useValue: spy }
16
+ ]
17
+ });
18
+ service = TestBed.inject(ColorLightenDarkenService);
19
+ textColorServiceSpy = TestBed.inject(TextColorService) as jasmine.SpyObj<TextColorService>;
20
+ });
21
+
22
+ it('should be created', () => {
23
+ expect(service).toBeTruthy();
24
+ });
25
+
26
+ it('should lighten color by 20%', () => {
27
+ // Mocking fixColor to return RGB values
28
+ textColorServiceSpy.fixColor.and.returnValue([52, 152, 219]);
29
+
30
+ const result = service.lighten('#3498db', 0.2);
31
+ expect(result).toMatch(/#[0-9A-Fa-f]{6}/); // Check if it's a valid hex color
32
+ // Here we would typically validate the exact color, but due to rounding, exact match might be complex.
33
+ // Instead, we could check if it's lighter by comparing HSL values, but that's beyond simple testing.
34
+ });
35
+
36
+ it('should darken color by 20%', () => {
37
+ // Mocking fixColor to return RGB values
38
+ textColorServiceSpy.fixColor.and.returnValue([52, 152, 219]);
39
+
40
+ const result = service.darken('#3498db', 0.2);
41
+ expect(result).toMatch(/#[0-9A-Fa-f]{6}/); // Check if it's a valid hex color
42
+ // Similar to lighten, we would ideally check if it's darker, but exact match is complicated by rounding.
43
+ });
44
+
45
+ it('should not exceed lightness value of 1', () => {
46
+ // Test with an already light color to ensure it doesn't go over 100%
47
+ textColorServiceSpy.fixColor.and.returnValue([255, 255, 255]); // White
48
+
49
+ const result = service.lighten('#ffffff', 0.5);
50
+ expect(result).toBe('#ffffff'); // Should remain at maximum lightness
51
+ });
52
+
53
+ it('should handle negative amount in lighten to darken', () => {
54
+ // This tests if lighten with negative amount behaves like darken
55
+ textColorServiceSpy.fixColor.and.returnValue([52, 152, 219]);
56
+
57
+ const result = service.lighten('#3498db', -0.2);
58
+ const darkenResult = service.darken('#3498db', 0.2);
59
+ expect(result).toEqual(darkenResult);
60
+ });
61
+ });
@@ -0,0 +1,83 @@
1
+ import { Injectable, inject } from '@angular/core';
2
+ import { TextColorService } from './text-color.service';
3
+
4
+ @Injectable({
5
+ providedIn: 'root'
6
+ })
7
+ export class ColorLightenDarkenService {
8
+
9
+ colors = inject(TextColorService)
10
+
11
+ // const color = '#3498db'; // Your color
12
+ // const lighterColor = lighten(color, 0.2); // 20% lighter
13
+ // const darkerColor = darken(color, 0.2); // 20% darker
14
+
15
+ // console.log(lighterColor, darkerColor);
16
+
17
+ constructor() { }
18
+
19
+ lighten(color: string, amount: number) {
20
+
21
+ const rgb = this.colors.fixColor(color)
22
+ // const rgb = color.match(/\w\w/g)?.map((x) => parseInt(x, 16)) || [];
23
+
24
+ // Convert RGB to HSL
25
+ let [r, g, b] = rgb.map((c) => c / 255);
26
+ const max = Math.max(r, g, b),
27
+ min = Math.min(r, g, b);
28
+ let h = 0,
29
+ s = 0,
30
+ l = (max + min) / 2;
31
+
32
+ if (max !== min) {
33
+ const d = max - min;
34
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
35
+ switch (max) {
36
+ case r:
37
+ h = (g - b) / d + (g < b ? 6 : 0);
38
+ break;
39
+ case g:
40
+ h = (b - r) / d + 2;
41
+ break;
42
+ case b:
43
+ h = (r - g) / d + 4;
44
+ break;
45
+ }
46
+ h /= 6;
47
+ }
48
+
49
+ // Modify the lightness and clamp it to [0, 1]
50
+ l = Math.min(1, l + amount);
51
+
52
+ // Convert HSL back to RGB
53
+ if (s === 0) {
54
+ r = g = b = l; // achromatic
55
+ } else {
56
+ const hue2rgb = (p: number, q: number, t: number) => {
57
+ if (t < 0) t += 1;
58
+ if (t > 1) t -= 1;
59
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
60
+ if (t < 1 / 2) return q;
61
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
62
+ return p;
63
+ };
64
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
65
+ const p = 2 * l - q;
66
+ r = hue2rgb(p, q, h + 1 / 3);
67
+ g = hue2rgb(p, q, h);
68
+ b = hue2rgb(p, q, h - 1 / 3);
69
+ }
70
+
71
+ // Convert RGB back to hexadecimal color
72
+ const toHex = (x: number) =>
73
+ Math.round(x * 255)
74
+ .toString(16)
75
+ .padStart(2, '0');
76
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
77
+ }
78
+
79
+ darken(color: string, amount: number) {
80
+ return this.lighten(color, -amount);
81
+ }
82
+
83
+ }
@@ -0,0 +1,85 @@
1
+ import { fakeAsync, TestBed, tick } from '@angular/core/testing';
2
+ import { ColorPalletteService } from './color-pallette.service';
3
+ import { BehaviorSubject } from 'rxjs';
4
+ import { ColorConversionService } from './color-conversion.service';
5
+
6
+ describe('ColorPalletteService', () => {
7
+ let service: ColorPalletteService;
8
+ let palletteBehaviorMock: jasmine.SpyObj<BehaviorSubject<{ color: string, complementaryColor: string }[]>>;
9
+ let colorConversionService: jasmine.SpyObj<ColorConversionService>;
10
+
11
+ beforeEach(() => {
12
+ const colorConversionServiceSpy = jasmine.createSpyObj('ColorConversionService', ['rgbToHex', 'hexToRgb']);
13
+ palletteBehaviorMock = jasmine.createSpyObj('BehaviorSubject', ['next', 'subscribe']);
14
+ TestBed.configureTestingModule({
15
+ providers: [ColorPalletteService,
16
+ {
17
+ provide: BehaviorSubject,
18
+ useValue: palletteBehaviorMock
19
+ },
20
+ {
21
+ provide: ColorConversionService,
22
+ useValue: colorConversionServiceSpy
23
+ }
24
+ ]
25
+ });
26
+ service = TestBed.inject(ColorPalletteService);
27
+ colorConversionService = TestBed.inject(ColorConversionService) as jasmine.SpyObj<ColorConversionService>;
28
+ });
29
+
30
+ it('should be created', () => {
31
+ expect(service).toBeTruthy();
32
+ });
33
+
34
+ it('should handle null context gracefully', fakeAsync(() => {
35
+ const image = new Image();
36
+ const colorCount = 5;
37
+ service.getColorsFromImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAwAB/gbKkAAAAABJRU5ErkJggg==');
38
+
39
+ image.onload = () => {
40
+ // Mock the canvas context to return null
41
+ const canvas = document.createElement('canvas');
42
+ spyOn(canvas, 'getContext').and.returnValue(null);
43
+ spyOn(document, 'createElement').and.returnValue(canvas);
44
+ const palette = service.generateColorPalette(image, colorCount);
45
+ expect(palette).toBeUndefined();
46
+ };
47
+ tick(1000);
48
+
49
+ // Trigger the image load
50
+ image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAwAB/gbKkAAAAABJRU5ErkJggg==';
51
+ }));
52
+
53
+ it('should quantize colors using k-means', () => {
54
+ const colors = [[255, 0, 0], [0, 255, 0], [0, 0, 255]];
55
+ const colorCount = 5;
56
+ const quantizedColors = (service as any).kMeansColorQuantization(colors, colorCount);
57
+ expect(quantizedColors.length).toEqual(5);
58
+ });
59
+
60
+ it('should sort colors by luminance', () => {
61
+ const colors = [[255, 0, 0], [0, 255, 0], [0, 0, 255]];
62
+ colors.sort((color1, color2) => {
63
+ const luminance1 = (service as any).getLuminance(color1);
64
+ const luminance2 = (service as any).getLuminance(color2);
65
+ return luminance2 - luminance1;
66
+ });
67
+ expect(colors[0]).toEqual([0, 255, 0]);
68
+ });
69
+
70
+ it('should call the color conversion service', () => {
71
+ const color = [255, 0, 0];
72
+ const hexColor = '#FF0000';
73
+ colorConversionService.rgbToHex.and.returnValue(hexColor);
74
+ const result = colorConversionService.rgbToHex(color);
75
+ expect(result).toEqual(hexColor);
76
+ });
77
+
78
+ it('should call behavior subject next', () => {
79
+ const data = [{ color: '#FF0000', complementaryColor: '#00FF00' }];
80
+
81
+ (service as any).palette = palletteBehaviorMock;
82
+ (service as any).palette.next(data);
83
+ expect(palletteBehaviorMock.next).toHaveBeenCalledWith(data);
84
+ });
85
+ });