color-util-helpers 1.0.4 → 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.
- package/README.md +47 -0
- package/ng-package.json +8 -0
- package/package.json +2 -15
- package/src/lib/assets/picture.webp +0 -0
- package/src/lib/color-conversion.service.spec.ts +54 -0
- package/src/lib/color-conversion.service.ts +35 -0
- package/src/lib/color-extractor.directive.spec.ts +49 -0
- package/src/lib/color-extractor.directive.ts +28 -0
- package/src/lib/color-grab.directive.ts +204 -0
- package/src/lib/color-lighten-darken.service.spec.ts +61 -0
- package/src/lib/color-lighten-darken.service.ts +83 -0
- package/src/lib/color-pallette.service.spec.ts +85 -0
- package/src/lib/color-pallette.service.ts +191 -0
- package/src/lib/color-scheme.service.ts +123 -0
- package/src/lib/color-utilities-demo/color-utilities-demo.component.css +12 -0
- package/src/lib/color-utilities-demo/color-utilities-demo.component.html +109 -0
- package/src/lib/color-utilities-demo/color-utilities-demo.component.ts +57 -0
- package/src/lib/color-utils.module.ts +27 -0
- package/src/lib/text-color.service.spec.ts +75 -0
- package/src/lib/text-color.service.ts +101 -0
- package/{public-api.d.ts → src/public-api.ts} +7 -0
- package/tsconfig.lib.json +32 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +14 -0
- package/color-util-helpers-1.0.4.tgz +0 -0
- package/esm2022/color-util-helpers.mjs +0 -5
- package/esm2022/lib/color-conversion.service.mjs +0 -39
- package/esm2022/lib/color-extractor.directive.mjs +0 -37
- package/esm2022/lib/color-lighten-darken.service.mjs +0 -79
- package/esm2022/lib/color-pallette.service.mjs +0 -172
- package/esm2022/lib/color-scheme.service.mjs +0 -113
- package/esm2022/lib/color-utilities-demo/color-utilities-demo.component.mjs +0 -41
- package/esm2022/lib/color-utils.module.mjs +0 -32
- package/esm2022/lib/text-color.service.mjs +0 -79
- package/esm2022/public-api.mjs +0 -12
- package/fesm2022/color-util-helpers.mjs +0 -580
- package/fesm2022/color-util-helpers.mjs.map +0 -1
- package/index.d.ts +0 -5
- package/lib/color-conversion.service.d.ts +0 -8
- package/lib/color-extractor.directive.d.ts +0 -13
- package/lib/color-lighten-darken.service.d.ts +0 -10
- package/lib/color-pallette.service.d.ts +0 -36
- package/lib/color-scheme.service.d.ts +0 -45
- package/lib/color-utilities-demo/color-utilities-demo.component.d.ts +0 -34
- package/lib/color-utils.module.d.ts +0 -10
- package/lib/text-color.service.d.ts +0 -18
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Injectable({
|
|
4
|
+
providedIn: 'root'
|
|
5
|
+
})
|
|
6
|
+
export class ColorSchemeService {
|
|
7
|
+
|
|
8
|
+
constructor() { }
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generates a random hexadecimal color code.
|
|
12
|
+
*
|
|
13
|
+
* This function generates a random hue value between 0 and 360 degrees, and random saturation and lightness values between 50% and 100%. It then converts the HSL values to RGB values using the `hslToRgb` function, and finally converts the RGB values to a hexadecimal color code using the `rgbToHex` function.
|
|
14
|
+
*
|
|
15
|
+
* @returns A hexadecimal color code in the format "#RRGGBB".
|
|
16
|
+
*/
|
|
17
|
+
generateRandomColor() {
|
|
18
|
+
// Generate a random hue between 0 and 360 (representing degrees on the color wheel)
|
|
19
|
+
const hue = Math.floor(Math.random() * 360);
|
|
20
|
+
|
|
21
|
+
// Generate random saturation and lightness values between 50% and 100%
|
|
22
|
+
const saturation = Math.floor(Math.random() * 51) + 50;
|
|
23
|
+
const lightness = Math.floor(Math.random() * 51) + 50;
|
|
24
|
+
|
|
25
|
+
// Convert HSL values to RGB values
|
|
26
|
+
const rgbColor = this.hslToRgb(hue, saturation, lightness);
|
|
27
|
+
|
|
28
|
+
// Convert RGB values to hexadecimal color code
|
|
29
|
+
const hexColor = this.rgbToHex(rgbColor.r, rgbColor.g, rgbColor.b);
|
|
30
|
+
|
|
31
|
+
return hexColor;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Converts HSL (Hue, Saturation, Lightness) color values to RGB (Red, Green, Blue) color values.
|
|
36
|
+
*
|
|
37
|
+
* @param h - The hue value, ranging from 0 to 360 degrees.
|
|
38
|
+
* @param s - The saturation value, ranging from 0 to 100 percent.
|
|
39
|
+
* @param l - The lightness value, ranging from 0 to 100 percent.
|
|
40
|
+
* @returns An object with the RGB color values, where each value is between 0 and 255.
|
|
41
|
+
*/
|
|
42
|
+
hslToRgb(h: number, s: number, l: number) {
|
|
43
|
+
h /= 360;
|
|
44
|
+
s /= 100;
|
|
45
|
+
l /= 100;
|
|
46
|
+
|
|
47
|
+
let r, g, b;
|
|
48
|
+
|
|
49
|
+
if (s === 0) {
|
|
50
|
+
r = g = b = l; // Achromatic color (gray)
|
|
51
|
+
} else {
|
|
52
|
+
const hueToRgb = (p: number, q: number, t: number) => {
|
|
53
|
+
if (t < 0) t += 1;
|
|
54
|
+
if (t > 1) t -= 1;
|
|
55
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
56
|
+
if (t < 1 / 2) return q;
|
|
57
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
58
|
+
return p;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
62
|
+
const p = 2 * l - q;
|
|
63
|
+
|
|
64
|
+
r = Math.round(hueToRgb(p, q, h + 1 / 3) * 255);
|
|
65
|
+
g = Math.round(hueToRgb(p, q, h) * 255);
|
|
66
|
+
b = Math.round(hueToRgb(p, q, h - 1 / 3) * 255);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { r, g, b };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Converts RGB color values to a hexadecimal color string.
|
|
74
|
+
*
|
|
75
|
+
* @param r - The red color value, between 0 and 255.
|
|
76
|
+
* @param g - The green color value, between 0 and 255.
|
|
77
|
+
* @param b - The blue color value, between 0 and 255.
|
|
78
|
+
* @returns A hexadecimal color string in the format "#RRGGBB".
|
|
79
|
+
*/
|
|
80
|
+
rgbToHex(r: number, g: number, b: number) {
|
|
81
|
+
const componentToHex = (c: number) => {
|
|
82
|
+
const hex = c.toString(16);
|
|
83
|
+
return hex.length === 1 ? "0" + hex : hex;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Adjusts a hexadecimal color value by a given percentage.
|
|
91
|
+
*
|
|
92
|
+
* @param hexColor - The hexadecimal color value to adjust.
|
|
93
|
+
* @param percentage - The percentage to adjust the color by, ranging from -100 to 100.
|
|
94
|
+
* @returns The adjusted hexadecimal color value.
|
|
95
|
+
*/
|
|
96
|
+
adjustHexColor(hexColor: string, percentage: number) {
|
|
97
|
+
// Remove the "#" symbol if present
|
|
98
|
+
hexColor = hexColor.replace("#", "");
|
|
99
|
+
|
|
100
|
+
// Convert the hex color to RGB values
|
|
101
|
+
const red = parseInt(hexColor.substring(0, 2), 16);
|
|
102
|
+
const green = parseInt(hexColor.substring(2, 4), 16);
|
|
103
|
+
const blue = parseInt(hexColor.substring(4, 6), 16);
|
|
104
|
+
|
|
105
|
+
// Calculate the adjustment amount based on the percentage
|
|
106
|
+
const adjustAmount = Math.round(255 * (percentage / 100));
|
|
107
|
+
|
|
108
|
+
// Adjust the RGB values
|
|
109
|
+
const adjustedRed = this.clamp(red + adjustAmount);
|
|
110
|
+
const adjustedGreen = this.clamp(green + adjustAmount);
|
|
111
|
+
const adjustedBlue = this.clamp(blue + adjustAmount);
|
|
112
|
+
|
|
113
|
+
// Convert the adjusted RGB values back to hex
|
|
114
|
+
const adjustedHexColor = `#${(adjustedRed).toString(16).padStart(2, '0')}${(adjustedGreen).toString(16).padStart(2, '0')}${(adjustedBlue).toString(16).padStart(2, '0')}`;
|
|
115
|
+
|
|
116
|
+
return adjustedHexColor;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
clamp(value: number) {
|
|
120
|
+
return Math.max(0, Math.min(value, 255));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<div style="margin: 2rem;">
|
|
2
|
+
|
|
3
|
+
<h1>Color Conversion Service</h1>
|
|
4
|
+
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
5
|
+
<div style="display: flex">
|
|
6
|
+
<div style="padding-top: .5rem; margin-right: .5rem;">rgbToHex: {{ HEX }}</div>
|
|
7
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="HEX"></div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div style="display: flex">
|
|
11
|
+
<div style="padding-top: .5rem; margin-right: .5rem;"> hexToRgb: {{ RGB }} </div>
|
|
12
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="RGB"></div>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div style="margin-top: 1rem; margin-bottom: 1rem;">
|
|
17
|
+
<mat-divider></mat-divider>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<h1>Color Light/Darken Service</h1>
|
|
21
|
+
|
|
22
|
+
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
23
|
+
|
|
24
|
+
<div style="display: flex; gap: 1rem">
|
|
25
|
+
Original Color: #AA11BB<br>
|
|
26
|
+
<div style="width: 32px; height: 32px; background-color: #AA11BB;"></div>
|
|
27
|
+
Lighten (25%): {{ lighten }}<br>
|
|
28
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="lighten"></div>
|
|
29
|
+
Darken (25%): {{ darken }}<br>
|
|
30
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="darken"></div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div style="margin-top: 1rem; margin-bottom: 1rem;">
|
|
36
|
+
<mat-divider></mat-divider>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<h1>Text Color Utility Services</h1>
|
|
40
|
+
|
|
41
|
+
<div style="display: flex; gap: 1rem; flex-direction: column;">
|
|
42
|
+
<div style="display: flex; gap: 1rem">
|
|
43
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="darken"></div>
|
|
44
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="lighten"></div>
|
|
45
|
+
is Darker : {{ colorIsDarker }}
|
|
46
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="colorIsDarker"></div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div style="display: flex; gap: 1rem; flex-direction: column;">
|
|
50
|
+
|
|
51
|
+
<div>
|
|
52
|
+
Use: {{ lightBk }} for '{{ HEX }}' background-color<br>
|
|
53
|
+
<div style="padding: 1rem;" [style.backgroundColor]="HEX" [style.color]="darkBk">
|
|
54
|
+
Sample Text Color
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div>
|
|
59
|
+
Use: {{ lightBk }} for 'whitesmoke' background-color<br>
|
|
60
|
+
<div style="padding: 1rem; background-color: whitesmoke;" [style.color]="lightBk">
|
|
61
|
+
Sample Text Color
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div style="margin-top: 1rem; margin-bottom: 1rem;">
|
|
68
|
+
<mat-divider></mat-divider>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<h1>Color Schema Services</h1>
|
|
72
|
+
|
|
73
|
+
<div style="display: flex; gap: 1rem">
|
|
74
|
+
Pick Color: {{ colorPick }}<br>
|
|
75
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="colorPick"></div>
|
|
76
|
+
Lighter Version: {{ colorPickLighter }}<br>
|
|
77
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="colorPickLighter"></div>
|
|
78
|
+
DarkerVersion: {{ colorPickDarker }}<br>
|
|
79
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="colorPickDarker"></div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div style="margin-top: 1rem; margin-bottom: 1rem;">
|
|
83
|
+
<mat-divider></mat-divider>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<h1>Color Pallette Service</h1>
|
|
87
|
+
Creates Pallette from Image
|
|
88
|
+
<div style="display: flex; gap: 2rem;">
|
|
89
|
+
<div>
|
|
90
|
+
<img [src]="img" height="180">
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div style="display: flex; gap: .5rem; width: 120px; border: 1px solid black; flex-wrap: wrap; padding: .5rem;">
|
|
94
|
+
<div>Color Pick</div>
|
|
95
|
+
<ng-container *ngFor="let color of (colors$ | async)">
|
|
96
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="color.color"></div>
|
|
97
|
+
</ng-container>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div style="display: flex; gap: .5rem; width: 120px; border: 1px solid black; flex-wrap: wrap; padding: .5rem;">
|
|
101
|
+
<div>Complementary</div>
|
|
102
|
+
<ng-container *ngFor="let color of (colors$ | async)">
|
|
103
|
+
<div style="width: 32px; height: 32px;" [style.backgroundColor]="color.complementaryColor"></div>
|
|
104
|
+
</ng-container>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
</div>
|
|
109
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Component, OnInit, inject } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
import { ColorConversionService } from '../color-conversion.service';
|
|
4
|
+
import { ColorPalletteService } from '../color-pallette.service';
|
|
5
|
+
import { TextColorService } from '../text-color.service';
|
|
6
|
+
import { ColorLightenDarkenService } from '../color-lighten-darken.service';
|
|
7
|
+
import { ColorSchemeService } from '../color-scheme.service';
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
selector: 'app-color-utilities-demo',
|
|
11
|
+
templateUrl: './color-utilities-demo.component.html',
|
|
12
|
+
styleUrls: ['./color-utilities-demo.component.css'],
|
|
13
|
+
})
|
|
14
|
+
export class ColorUtilitiesDemoComponent implements OnInit {
|
|
15
|
+
|
|
16
|
+
colorConversionService = inject(ColorConversionService)
|
|
17
|
+
colorLightenDarkenService = inject(ColorLightenDarkenService)
|
|
18
|
+
colorPalletteService = inject(ColorPalletteService)
|
|
19
|
+
textColorService = inject(TextColorService)
|
|
20
|
+
colorSchemeService = inject(ColorSchemeService)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
HEX = this.colorConversionService.rgbToHex([12, 56, 128])
|
|
24
|
+
RGB = `rgb(${this.colorConversionService.hexToRgb('#AA11BB')})`
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
lighten = this.colorLightenDarkenService.lighten('#AA11BB', .25)
|
|
28
|
+
darken = this.colorLightenDarkenService.darken('#AA11BB', .25)
|
|
29
|
+
|
|
30
|
+
colorIsDarker = this.textColorService.isColorDarker(this.lighten, this.darken)
|
|
31
|
+
|
|
32
|
+
darkBk = this.textColorService.textColorForBgColor(this.HEX, this.lighten, this.darken)
|
|
33
|
+
lightBk = this.textColorService.textColorForBgColor('whitesmoke', this.lighten, this.darken)
|
|
34
|
+
|
|
35
|
+
palette: any
|
|
36
|
+
colors$ = this.colorPalletteService.palette$
|
|
37
|
+
|
|
38
|
+
colorPick = this.colorSchemeService.generateRandomColor()
|
|
39
|
+
colorPickDarker = this.colorSchemeService.adjustHexColor(this.colorPick, -25)
|
|
40
|
+
colorPickLighter = this.colorSchemeService.adjustHexColor(this.colorPick, 25)
|
|
41
|
+
|
|
42
|
+
img: string|any
|
|
43
|
+
|
|
44
|
+
constructor() { }
|
|
45
|
+
|
|
46
|
+
ngOnInit() {
|
|
47
|
+
|
|
48
|
+
// define image path
|
|
49
|
+
this.img = 'assets/picture.webp'
|
|
50
|
+
this.colorPalletteService.getColorsFromImage(this.img, 8)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
|
|
4
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
5
|
+
import { MatDividerModule } from '@angular/material/divider';
|
|
6
|
+
import { ColorGrabberDirective } from './color-grab.directive';
|
|
7
|
+
|
|
8
|
+
import { ColorUtilitiesDemoComponent } from './color-utilities-demo/color-utilities-demo.component';
|
|
9
|
+
import { ColorExtractorDirective } from './color-extractor.directive';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@NgModule({
|
|
13
|
+
imports: [
|
|
14
|
+
CommonModule,
|
|
15
|
+
MatButtonModule,
|
|
16
|
+
MatDividerModule
|
|
17
|
+
],
|
|
18
|
+
declarations: [
|
|
19
|
+
ColorUtilitiesDemoComponent,
|
|
20
|
+
ColorGrabberDirective,
|
|
21
|
+
ColorExtractorDirective
|
|
22
|
+
],
|
|
23
|
+
exports: [
|
|
24
|
+
ColorUtilitiesDemoComponent
|
|
25
|
+
]
|
|
26
|
+
})
|
|
27
|
+
export class ColorUtilitiesModule { }
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { TextColorService } from './text-color.service';
|
|
3
|
+
import { ColorConversionService } from './color-conversion.service';
|
|
4
|
+
|
|
5
|
+
describe('TextColorService', () => {
|
|
6
|
+
let service: TextColorService;
|
|
7
|
+
let colorConversionServiceSpy: jasmine.SpyObj<ColorConversionService>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
const spy = jasmine.createSpyObj('ColorConversionService', ['hexToRgb']);
|
|
11
|
+
|
|
12
|
+
TestBed.configureTestingModule({
|
|
13
|
+
providers: [
|
|
14
|
+
TextColorService,
|
|
15
|
+
{ provide: ColorConversionService, useValue: spy }
|
|
16
|
+
]
|
|
17
|
+
});
|
|
18
|
+
service = TestBed.inject(TextColorService);
|
|
19
|
+
colorConversionServiceSpy = TestBed.inject(ColorConversionService) as jasmine.SpyObj<ColorConversionService>;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should be created', () => {
|
|
23
|
+
expect(service).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return dark text for light background', () => {
|
|
27
|
+
colorConversionServiceSpy.hexToRgb.and.returnValue([255, 255, 255]); // White
|
|
28
|
+
expect(service.textColorForBgColor('#FFFFFF', '#FFFFFF', '#000000')).toBe('#000000');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return light text for dark background', () => {
|
|
32
|
+
colorConversionServiceSpy.hexToRgb.and.returnValue([0, 0, 0]); // Black
|
|
33
|
+
expect(service.textColorForBgColor('#000000', '#FFFFFF', '#000000')).toBe('#FFFFFF');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should determine darker color correctly', () => {
|
|
37
|
+
colorConversionServiceSpy.hexToRgb.withArgs('#0000FF').and.returnValue([0, 0, 255]); // Blue
|
|
38
|
+
colorConversionServiceSpy.hexToRgb.withArgs('#FF0000').and.returnValue([255, 0, 0]); // Red
|
|
39
|
+
expect(service.darkerColor('#0000FF', '#FF0000')).toBe('#0000FF');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should determine lighter color correctly', () => {
|
|
43
|
+
colorConversionServiceSpy.hexToRgb.withArgs('#0000FF').and.returnValue([0, 0, 255]); // Blue
|
|
44
|
+
colorConversionServiceSpy.hexToRgb.withArgs('#FF0000').and.returnValue([255, 0, 0]); // Red
|
|
45
|
+
expect(service.lighterColor('#FF0000', '#0000FF')).toBe('#FF0000');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should calculate luminance accurately', () => {
|
|
49
|
+
expect(service.calculateLuminance(255, 255, 255)).toBeCloseTo(255, 2); // White
|
|
50
|
+
expect(service.calculateLuminance(0, 0, 0)).toBeCloseTo(0, 2); // Black
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should fix color to RGB array for hex input', () => {
|
|
54
|
+
colorConversionServiceSpy.hexToRgb.withArgs('#FF0000').and.returnValue([255, 0, 0]);
|
|
55
|
+
expect(service.fixColor('#FF0000')).toEqual([255, 0, 0]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should handle 3-digit hex color', () => {
|
|
59
|
+
colorConversionServiceSpy.hexToRgb.withArgs('#F00').and.returnValue([255, 0, 0]);
|
|
60
|
+
expect(service.fixColor('#F00')).toEqual([255, 0, 0]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return empty array for invalid hex color', () => {
|
|
64
|
+
colorConversionServiceSpy.hexToRgb.and.returnValue([]);
|
|
65
|
+
expect(service.fixColor('#GGG')).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should convert RGB string to array', () => {
|
|
69
|
+
expect(service.fixColor('rgb(100, 200, 50)')).toEqual([100, 200, 50]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle malformed color input', () => {
|
|
73
|
+
expect(service.fixColor('not a color')).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
});
|