color-util-helpers 1.0.7 → 1.0.10
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/README.md +6 -2
- package/color-util-helpers-1.0.10.tgz +0 -0
- package/esm2022/color-util-helpers.mjs +5 -0
- package/esm2022/lib/color-conversion.service.mjs +39 -0
- package/esm2022/lib/color-extractor.directive.mjs +37 -0
- package/esm2022/lib/color-grab.directive.mjs +185 -0
- package/esm2022/lib/color-lighten-darken.service.mjs +79 -0
- package/esm2022/lib/color-pallette.service.mjs +172 -0
- package/esm2022/lib/color-scheme.service.mjs +113 -0
- package/esm2022/lib/color-utilities-demo/color-utilities-demo.component.mjs +41 -0
- package/esm2022/lib/color-utils.module.mjs +38 -0
- package/esm2022/lib/text-color.service.mjs +79 -0
- package/esm2022/public-api.mjs +13 -0
- package/fesm2022/color-util-helpers.mjs +767 -0
- package/fesm2022/color-util-helpers.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/color-conversion.service.d.ts +8 -0
- package/lib/color-extractor.directive.d.ts +13 -0
- package/lib/color-grab.directive.d.ts +31 -0
- package/lib/color-lighten-darken.service.d.ts +10 -0
- package/lib/color-pallette.service.d.ts +36 -0
- package/lib/color-scheme.service.d.ts +45 -0
- package/lib/color-utilities-demo/color-utilities-demo.component.d.ts +34 -0
- package/lib/color-utils.module.d.ts +12 -0
- package/lib/text-color.service.d.ts +18 -0
- package/package.json +15 -2
- package/{src/public-api.ts → public-api.d.ts} +0 -6
- package/ng-package.json +0 -8
- package/src/lib/assets/picture.webp +0 -0
- package/src/lib/color-conversion.service.spec.ts +0 -54
- package/src/lib/color-conversion.service.ts +0 -35
- package/src/lib/color-extractor.directive.spec.ts +0 -49
- package/src/lib/color-extractor.directive.ts +0 -28
- package/src/lib/color-grab.directive.ts +0 -204
- package/src/lib/color-lighten-darken.service.spec.ts +0 -61
- package/src/lib/color-lighten-darken.service.ts +0 -83
- package/src/lib/color-pallette.service.spec.ts +0 -85
- package/src/lib/color-pallette.service.ts +0 -191
- package/src/lib/color-scheme.service.ts +0 -123
- package/src/lib/color-utilities-demo/color-utilities-demo.component.css +0 -12
- package/src/lib/color-utilities-demo/color-utilities-demo.component.html +0 -109
- package/src/lib/color-utilities-demo/color-utilities-demo.component.ts +0 -57
- package/src/lib/color-utils.module.ts +0 -27
- package/src/lib/text-color.service.spec.ts +0 -75
- package/src/lib/text-color.service.ts +0 -101
- package/tsconfig.lib.json +0 -32
- package/tsconfig.lib.prod.json +0 -10
- package/tsconfig.spec.json +0 -14
|
@@ -1,204 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { Injectable } from '@angular/core'
|
|
2
|
-
import { BehaviorSubject } from 'rxjs'
|
|
3
|
-
import { ColorConversionService } from './color-conversion.service'
|
|
4
|
-
|
|
5
|
-
@Injectable({
|
|
6
|
-
providedIn: 'root'
|
|
7
|
-
})
|
|
8
|
-
export class ColorPalletteService {
|
|
9
|
-
|
|
10
|
-
// define image path
|
|
11
|
-
// this.colorSelectionService.getColorsFromImage('../assets/sample2.jpg')
|
|
12
|
-
|
|
13
|
-
// get colors
|
|
14
|
-
// this.colorSelectionService.palette.subscribe(data => this.palette = data)
|
|
15
|
-
|
|
16
|
-
// sample html
|
|
17
|
-
// <div *ngFor="let color of palette">
|
|
18
|
-
// <div style="display: flex;">
|
|
19
|
-
// <div
|
|
20
|
-
// class="box"
|
|
21
|
-
// [style.background-color]="color.color"
|
|
22
|
-
// >
|
|
23
|
-
// Color
|
|
24
|
-
// </div>
|
|
25
|
-
// <div
|
|
26
|
-
// class="box"
|
|
27
|
-
// [style.background-color]="color.complementaryColor"
|
|
28
|
-
// >
|
|
29
|
-
// Complementary
|
|
30
|
-
// </div>
|
|
31
|
-
// </div>
|
|
32
|
-
// </div>
|
|
33
|
-
|
|
34
|
-
// CSS
|
|
35
|
-
// .box {
|
|
36
|
-
// width: 100px;
|
|
37
|
-
// height: 100px;
|
|
38
|
-
// border: solid thin black;
|
|
39
|
-
// color: black;
|
|
40
|
-
// margin: 4px;
|
|
41
|
-
// padding: 16px;
|
|
42
|
-
// display: flex;
|
|
43
|
-
// flex-wrap: wrap;
|
|
44
|
-
// align-content: center;
|
|
45
|
-
// justify-content: center;
|
|
46
|
-
// }
|
|
47
|
-
|
|
48
|
-
private palette = new BehaviorSubject<{ color: string, complementaryColor: string }[]>([])
|
|
49
|
-
palette$ = this.palette.asObservable()
|
|
50
|
-
|
|
51
|
-
constructor(
|
|
52
|
-
private colorConversionService: ColorConversionService
|
|
53
|
-
) { }
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Retrieves a color palette from an image at the specified path.
|
|
57
|
-
*
|
|
58
|
-
* @param imagePath - The path to the image to extract the color palette from.
|
|
59
|
-
* @param colors - The number of colors to include in the palette (default is 3).
|
|
60
|
-
* @returns An observable that emits the generated color palette.
|
|
61
|
-
*/
|
|
62
|
-
getColorsFromImage(imagePath: string, colors = 3) {
|
|
63
|
-
const image = new Image();
|
|
64
|
-
image.src = imagePath;
|
|
65
|
-
|
|
66
|
-
image.onload = () => {
|
|
67
|
-
const data = this.generateColorPalette(image, colors) || [];
|
|
68
|
-
this.palette.next(data);
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Generates a color palette from an image.
|
|
74
|
-
*
|
|
75
|
-
* @param image - The HTML image element to extract the color palette from.
|
|
76
|
-
* @param colorCount - The number of colors to include in the palette (default is 6).
|
|
77
|
-
* @returns An array of color objects, each with a hex color and a complementary hex color.
|
|
78
|
-
*/
|
|
79
|
-
generateColorPalette(image: HTMLImageElement, colorCount = 6) {
|
|
80
|
-
const canvas = document.createElement("canvas");
|
|
81
|
-
const context = canvas.getContext("2d");
|
|
82
|
-
|
|
83
|
-
if (!context) return;
|
|
84
|
-
|
|
85
|
-
canvas.width = image.width;
|
|
86
|
-
canvas.height = image.height;
|
|
87
|
-
context.drawImage(image, 0, 0);
|
|
88
|
-
|
|
89
|
-
// Get the image data
|
|
90
|
-
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
91
|
-
const pixels = imageData.data;
|
|
92
|
-
const pixelCount = imageData.width * imageData.height;
|
|
93
|
-
|
|
94
|
-
// Build an array of RGB colors
|
|
95
|
-
const colors = [];
|
|
96
|
-
for (let i = 0; i < pixelCount; i++) {
|
|
97
|
-
const offset = i * 4;
|
|
98
|
-
const r = pixels[offset];
|
|
99
|
-
const g = pixels[offset + 1];
|
|
100
|
-
const b = pixels[offset + 2];
|
|
101
|
-
colors.push([r, g, b]);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Apply color quantization using k-means clustering
|
|
105
|
-
const quantizedColors = this.kMeansColorQuantization(colors, colorCount);
|
|
106
|
-
|
|
107
|
-
// Order colors by luminance
|
|
108
|
-
quantizedColors.sort((color1, color2) => {
|
|
109
|
-
const luminance1 = this.getLuminance(color1);
|
|
110
|
-
const luminance2 = this.getLuminance(color2);
|
|
111
|
-
return luminance2 - luminance1;
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const palette = quantizedColors.map((color) => {
|
|
115
|
-
const complementaryColor = color.map((component) => 255 - component);
|
|
116
|
-
const hexColor = this.colorConversionService.rgbToHex(color);
|
|
117
|
-
const hexComplementaryColor =
|
|
118
|
-
this.colorConversionService.rgbToHex(complementaryColor);
|
|
119
|
-
return { color: hexColor, complementaryColor: hexComplementaryColor };
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
return palette;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private getLuminance(color: number[]) {
|
|
126
|
-
const [r, g, b] = color
|
|
127
|
-
return 0.299 * r + 0.587 * g + 0.114 * b
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
private calculateColorDistance(color1: number[], color2: number[]) {
|
|
131
|
-
const [r1, g1, b1] = color1
|
|
132
|
-
const [r2, g2, b2] = color2
|
|
133
|
-
const dr = r2 - r1
|
|
134
|
-
const dg = g2 - g1
|
|
135
|
-
const db = b2 - b1
|
|
136
|
-
return Math.sqrt(dr * dr + dg * dg + db * db)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private calculateMeanColor(colors: number[][]) {
|
|
140
|
-
let sumR = 0
|
|
141
|
-
let sumG = 0
|
|
142
|
-
let sumB = 0
|
|
143
|
-
for (let i = 0; i < colors.length; i++) {
|
|
144
|
-
const [r, g, b] = colors[i]
|
|
145
|
-
sumR += r
|
|
146
|
-
sumG += g
|
|
147
|
-
sumB += b
|
|
148
|
-
}
|
|
149
|
-
const meanR = Math.round(sumR / colors.length)
|
|
150
|
-
const meanG = Math.round(sumG / colors.length)
|
|
151
|
-
const meanB = Math.round(sumB / colors.length)
|
|
152
|
-
return [meanR, meanG, meanB]
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private kMeansColorQuantization(colors: number[][], k: number) {
|
|
156
|
-
let clusterCenters = []
|
|
157
|
-
for (let i = 0; i < k; i++) {
|
|
158
|
-
const randomColor = colors[Math.floor(Math.random() * colors.length)]
|
|
159
|
-
clusterCenters.push(randomColor)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
let clusters = []
|
|
163
|
-
for (let i = 0; i < colors.length; i++) {
|
|
164
|
-
const color = colors[i]
|
|
165
|
-
let minDistance = Infinity
|
|
166
|
-
let nearestCenter = null
|
|
167
|
-
for (let j = 0; j < clusterCenters.length; j++) {
|
|
168
|
-
const center = clusterCenters[j]
|
|
169
|
-
const distance = this.calculateColorDistance(color, center)
|
|
170
|
-
if (distance < minDistance) {
|
|
171
|
-
minDistance = distance
|
|
172
|
-
nearestCenter = center
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
clusters.push({ color, center: nearestCenter })
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
let updatedCenters = []
|
|
179
|
-
for (let i = 0; i < clusterCenters.length; i++) {
|
|
180
|
-
const center = clusterCenters[i]
|
|
181
|
-
const clusterColors = clusters.filter(c => c.center === center).map(c => c.color)
|
|
182
|
-
if (clusterColors.length > 0) {
|
|
183
|
-
const updatedCenter = this.calculateMeanColor(clusterColors)
|
|
184
|
-
updatedCenters.push(updatedCenter)
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return updatedCenters
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
}
|