coloration.ts 16.4 KB
Newer Older
Zéfling's avatar
Zéfling committed
1
2
3
4

// tslint:disable:no-bitwise

const pattern = {
Zéfling's avatar
Zéfling committed
5
    hexa: /^#?(([\da-f]{3})(([\da-f]{3})([\da-f]{2})?|[\da-f]{1})?)$/i,
Zéfling's avatar
Zéfling committed
6
7
    rgba: /^rgba?\(([\d]*(\.[\d]+)?)(,?\s*|\s+)([\d]*(\.[\d]+)?)(,?\s*|\s+)([\d]*(\.[\d]+)?)((,\s*)([\d]*(\.[\d]+)?))?\)$/i,
    hsva: /^hsla?\(([\d]*(\.[\d]+)?)(,?\s*|\s+)([\d]*(\.[\d]+)?)\%(,?\s*|\s+)([\d]*(\.[\d]+)?)\%((,\s*)([\d]*(\.[\d]+)?))?\)$/i
Zéfling's avatar
Zéfling committed
8
9
};

10
export interface RGB {
Zéfling's avatar
Zéfling committed
11
    /** Red color value [0,255]   */ r: number;
12
    /** Green color value [0,255] */ g: number;
Zéfling's avatar
Zéfling committed
13
14
    /** Blue color value [0,255]  */ b: number;
    /** Alpha color value [0,1]   */ a?: number;
15
16
}
export interface HSV {
Zéfling's avatar
Zéfling committed
17
18
19
20
    /** Hue color value [0,360]             */ h: number;
    /** Saluration color value [0,100]      */ s: number;
    /** Value/lightness color value [0,100] */ v: number;
    /** Alpha color value [0,1]             */ a?: number;
21
}
22
export interface ColorData {
23
    // RGB
Zéfling's avatar
Zéfling committed
24
25
26
    /** Red color value [-256,255]       */ r?: number;
    /** Green color value [-256,255]     */ g?: number;
    /** Blue color value [-256,255]      */ b?: number;
27
    // HSV/HSL
Zéfling's avatar
Zéfling committed
28
29
30
    /** Hue value [-360,360]             */ h?: number;
    /** Saluration value [-100,100]      */ s?: number;
    /** Value/lightness value [-100,100] */ v?: number;
31
    // Other
Zéfling's avatar
Zéfling committed
32
33
34
35
36
37
38
39
40
41
    /** luminosity value [-1,1]          */ luminosity?: number;
    /** color mask                       */ maskColor?: string;
    /** color mask opacity [0,1]         */ maskOpacity?: number;
    // alpha
    /** color mask alpha [-1,1]          */ alpha?: number;
}

export interface ColorNumber {
     /** intcolor */ intColor: number;
     /** Alpha color value [0,1] */ alpha: number;
42
43
}

Zéfling's avatar
Zéfling committed
44
45
export class Coloration {

46
47
48
    /**
     * CSS color list
     */
Zéfling's avatar
Zéfling committed
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
    static colorsName: any = {
        // CSS 1
        black: '#000000', silver: '#c0c0c0', gray: '#808080', white: '#ffffff', maroon: '#800000', red: '#ff0000',
        purple: '#800080', fuchsia: '#ff00ff', green: '#008000', lime: '#00ff00', olive: '#808000', yellow: '#ffff00',
        navy: '#000080', blue: '#0000ff', teal: '#008080', aqua: '#00ffff',
        // CSS 2 (Revision 1)
        orange: '#ffa500',
        // CSS 3
        aliceblue: '#f0f8ff', antiquewhite: '#faebd7', aquamarine: '#7fffd4', azure: '#f0ffff', beige: '#f5f5dc',
        bisque: '#ffe4c4', blanchedalmond: '#ffebcd', blueviolet: '#8a2be2', brown: '#a52a2a', burlywood: '#deb887',
        cadetblue: '#5f9ea0', chartreuse: '#7fff00', chocolate: '#d2691e', coral: '#ff7f50', cornflowerblue: '#6495ed',
        cornsilk: '#fff8dc', crimson: '#dc143c', cyan: '#00ffff', deaqua: '#00ffff', darkblue: '#00008b',
        darkcyan: '#008b8b', darkgoldenrod: '#b8860b', darkgray: '#a9a9a9', darkgreen: '#006400', darkgrey: '#a9a9a9',
        darkkhaki: '#bdb76b', darkmagenta: '#8b008b', darkolivegreen: '#556b2f', darkorange: '#ff8c00',
        darkorchid: '#9932cc', darkred: '#8b0000', darksalmon: '#e9967a', darkseagreen: '#8fbc8f',
        darkslateblue: '#483d8b', darkslategray: '#2f4f4f', darkslategrey: '#2f4f4f', darkturquoise: '#00ced1',
        darkviolet: '#9400d3', deeppink: '#ff1493', deepskyblue: '#00bfff', dimgray: '#696969', dimgrey: '#696969',
        dodgerblue: '#1e90ff', firebrick: '#b22222', floralwhite: '#fffaf0', forestgreen: '#228b22',
        gainsboro: '#dcdcdc', ghostwhite: '#f8f8ff', gold: '#ffd700', goldenrod: '#daa520', greenyellow: '#adff2f',
        grey: '#808080', honeydew: '#f0fff0', hotpink: '#ff69b4', indianred: '#cd5c5c', indigo: '#4b0082',
        ivory: '#fffff0', khaki: '#f0e68c', lavender: '#e6e6fa', lavenderblush: '#fff0f5', lawngreen: '#7cfc00',
        lemonchiffon: '#fffacd', lightblue: '#add8e6', lightcoral: '#f08080', lightcyan: '#e0ffff',
        lightgoldenrodyellow: '#fafad2', lightgray: '#d3d3d3', lightgreen: '#90ee90', lightgrey: '#d3d3d3',
        lightpink: '#ffb6c1', lightsalmon: '#ffa07a', lightseagreen: '#20b2aa', lightskyblue: '#87cefa',
        lightslategray: '#778899', lightslategrey: '#778899', lightsteelblue: '#b0c4de', lightyellow: '#ffffe0',
        limegreen: '#32cd32', linen: '#faf0e6', magenta: '#ff00ff', defuchsia: '#ff00ff', mediumaquamarine: '#66cdaa',
        mediumblue: '#0000cd', mediumorchid: '#ba55d3', mediumpurple: '#9370db', mediumseagreen: '#3cb371',
        mediumslateblue: '#7b68ee', mediumspringgreen: '#00fa9a', mediumturquoise: '#48d1cc',
        mediumvioletred: '#c71585', midnightblue: '#191970', mintcream: '#f5fffa', mistyrose: '#ffe4e1',
        moccasin: '#ffe4b5', navajowhite: '#ffdead', oldlace: '#fdf5e6', olivedrab: '#6b8e23', orangered: '#ff4500',
        orchid: '#da70d6', palegoldenrod: '#eee8aa', palegreen: '#98fb98', paleturquoise: '#afeeee',
        palevioletred: '#db7093', papayawhip: '#ffefd5', peachpuff: '#ffdab9', peru: '#cd853f', pink: '#ffc0cb',
        plum: '#dda0dd', powderblue: '#b0e0e6', rosybrown: '#bc8f8f', royalblue: '#4169e1', saddlebrown: '#8b4513',
        salmon: '#fa8072', sandybrown: '#f4a460', seagreen: '#2e8b57', seashell: '#fff5ee', sienna: '#a0522d',
        skyblue: '#87ceeb', slateblue: '#6a5acd', slategray: '#708090', slategrey: '#708090', snow: '#fffafa',
        springgreen: '#00ff7f', steelblue: '#4682b4', tan: '#d2b48c', thistle: '#d8bfd8', tomato: '#ff6347',
        turquoise: '#40e0d0', violet: '#ee82ee', wheat: '#f5deb3', whitesmoke: '#f5f5f5', yellowgreen: '#9acd32',
        // CSS 4
        rebeccapurple: '#663399'
    };

Zéfling's avatar
Zéfling committed
90
91
92
    private calcColor: ColorNumber;
    private rgb: RGB = { r: 0, g: 0, b: 0, a: 1 };
    private hsv: HSV = { h: 0, s: 0, v: 0, a: 1 };
Zéfling's avatar
Zéfling committed
93
94

    constructor(public color: string) {
Zéfling's avatar
Zéfling committed
95
96
97
98
99
100
101
        this.reset();
    }

    /**
     * reinit to base color
     */
    reset() {
Zéfling's avatar
Zéfling committed
102
103
104
105
        if (this.color) {
            this.calcColor = this.parseColor(this.color);
            this.updateColor();
        }
Zéfling's avatar
Zéfling committed
106
    }
Zéfling's avatar
Zéfling committed
107
108
109
110

    /**
     * change the luminosity of a color
     * @param lum value between -1 and 1
111
     * @returns Coloration
Zéfling's avatar
Zéfling committed
112
     */
113
114
115
116
    changeLuminosity(lum: number): Coloration {
        lum = this.minmax(lum, -1, 1);
        this.maskColor(lum < 0 ? '#000' : '#FFF', Math.abs(lum));
        return this;
Zéfling's avatar
Zéfling committed
117
118
119
120
121
122
    }

    /**
     * add color with a mark
     * @param color additional color
     * @param opacity value of opacity between 0 and 1 for the additional color
123
     * @returns Coloration
Zéfling's avatar
Zéfling committed
124
     */
125
    maskColor(color: string, opacity: number = 1): Coloration {
Zéfling's avatar
Zéfling committed
126
127
128
        if (this.calcColor) {
            const baseColor = this.calcColor;
            const additionalColor = this.parseColor(color);
Zéfling's avatar
Zéfling committed
129

Zéfling's avatar
Zéfling committed
130
            const lum = this.minmax(opacity, 0, 1);
Zéfling's avatar
Zéfling committed
131

Zéfling's avatar
Zéfling committed
132
133
134
            const R = baseColor.intColor >> 16;
            const G = baseColor.intColor >> 8 & 0x00FF;
            const B = baseColor.intColor & 0x0000FF;
135

Zéfling's avatar
Zéfling committed
136
            if (additionalColor.alpha) {
137
                this.calcColor.alpha = this.minmax(baseColor.alpha + additionalColor.alpha * opacity, 0, 1);
Zéfling's avatar
Zéfling committed
138
139
140
141
142
143
144
            }
            this.calcColor.intColor = this.rgbToInt(
                Math.round(((additionalColor.intColor >> 16) - R) * lum) + R,
                Math.round(((additionalColor.intColor >> 8 & 0x00FF) - G) * lum) + G,
                Math.round(((additionalColor.intColor & 0x0000FF) - B) * lum) + B
            );
            this.updateColor();
Zéfling's avatar
Zéfling committed
145
        }
146
147
148
149
150
151
        return this;
    }

    /**
     * change color with color parameters
     * @param colorData additionnal parameters
152
     * @returns Coloration
153
154
     */
    addColor(colorData: ColorData) {
Zéfling's avatar
Zéfling committed
155
156
157
        if (this.calcColor) {
            if (colorData.luminosity) {
                this.changeLuminosity(colorData.luminosity);
158
159
            }

Zéfling's avatar
Zéfling committed
160
161
            if (colorData.maskColor) {
                this.maskColor(colorData.maskColor, this.minmax(colorData.maskOpacity || 0, 0, 1));
162
            }
Zéfling's avatar
Zéfling committed
163
164
165
166
167
168
169
170
171
172
173
174
175

            if (colorData.r || colorData.g || colorData.b) {
                if (colorData.r) {
                    this.rgb.r = this.minmax(this.rgb.r + (+colorData.r), 0, 255);
                }
                if (colorData.g) {
                    this.rgb.g = this.minmax(this.rgb.g + (+colorData.g), 0, 255);
                }
                if (colorData.b) {
                    this.rgb.b = this.minmax(this.rgb.b + (+colorData.b), 0, 255);
                }
                this.calcColor.intColor = this.rgbToInt(this.rgb.r, this.rgb.g, this.rgb.b);
                this.updateColor();
176
            }
Zéfling's avatar
Zéfling committed
177
178
179
180
181
182
183
184
185
186
187
188
189

            if (colorData.h || colorData.s || colorData.v) {
                if (colorData.h) {
                    this.hsv.h = (this.hsv.h + (+colorData.h) + 360) % 360;
                }
                if (colorData.s) {
                    this.hsv.s = this.minmax(this.hsv.s + (+colorData.s), 0, 100);
                }
                if (colorData.v) {
                    this.hsv.v = this.minmax(this.hsv.v + (+colorData.v), 0, 100);
                }
                this.calcColor.intColor = this.hsvToInt(this.hsv.h, this.hsv.s, this.hsv.v);
                this.updateColor();
190
191
            }

Zéfling's avatar
Zéfling committed
192
193
194
            if (colorData.alpha) {
                this.hsv.a = this.rgb.a = this.calcColor.alpha = this.minmax(this.calcColor.alpha + (+colorData.alpha), 0, 1);
            }
Zéfling's avatar
Zéfling committed
195
        }
196
197
198
        return this;
    }

199
200
201
202
203
    /**
     * RGB informations
     * @returns RGB
     */
    getRGB(): RGB {
Zéfling's avatar
Zéfling committed
204
        return this.calcColor ? this.value<RGB>(() => Object.assign({}, this.rgb)) : null;
205
206
207
208
209
210
211
    }

    /**
     * HSV informations
     * @returns HSV
     */
    getHSV(): HSV {
Zéfling's avatar
Zéfling committed
212
        return this.calcColor ? this.value<HSV>(() => Object.assign({}, this.hsv)) : null;
213
214
    }

215
216
    /**
     * color in #HEX format
217
     * @returns string of #HEX
218
219
     */
    toHEX(): string {
Zéfling's avatar
Zéfling committed
220
221
        return this.calcColor ? this.value<string>(
            () => '#' + (0x1000000 + this.calcColor.intColor).toString(16).slice(1)
Zéfling's avatar
Zéfling committed
222
                + (this.calcColor.alpha < 1 ? (0x100 + Math.round(this.calcColor.alpha * 255)).toString(16).slice(1) : '')
Zéfling's avatar
Zéfling committed
223
        ) : null;
224
225
226
227
    }

    /**
     * color in HVL() format
228
     * @returns string of HVL(H, S%, V%)
229
230
     */
    toHSL(): string {
Zéfling's avatar
Zéfling committed
231
        return this.calcColor ? this.value<string>(
Zéfling's avatar
Zéfling committed
232
            () => `hsl(${this.hsv.h}, ${this.hsv.s}%, ${this.hsv.v}%${this.hsv.a < 1 ? ', ' + this.hsv.a : ''})`
Zéfling's avatar
Zéfling committed
233
        ) : null;
234
235
236
237
    }

    /**
     * color in RGB() format
238
     * @returns string of RGB(R, G, B)
239
240
     */
    toRGB(): string {
Zéfling's avatar
Zéfling committed
241
        return this.calcColor ? this.value<string>(
Zéfling's avatar
Zéfling committed
242
            () => `rgb(${this.rgb.r}, ${this.rgb.g}, ${this.rgb.b}${this.rgb.a < 1 ? ', ' + this.rgb.a : ''})`
Zéfling's avatar
Zéfling committed
243
        ) : null;
244
245
    }

246
247
248
    /**
     * update colors data : RGB and HSL
     */
249
    private updateColor() {
Zéfling's avatar
Zéfling committed
250
        const color = this.calcColor.intColor;
251
        // update RGB
Zéfling's avatar
Zéfling committed
252
253
254
255
        this.rgb.r = color >> 16;
        this.rgb.g = color >> 8 & 0x00FF;
        this.rgb.b = color & 0x0000FF;
        this.rgb.a = this.calcColor.alpha;
256
257
258

        // update HSL
        this.hsv = this.rgb2hsv(this.rgb.r, this.rgb.g, this.rgb.b);
Zéfling's avatar
Zéfling committed
259
        this.hsv.a = this.calcColor.alpha;
Zéfling's avatar
Zéfling committed
260
261
262
263
264
265
266
    }

    /**
     * change color
     * @param color parse color : name, #HEXA, rgba()
     * @returns int color
     */
Zéfling's avatar
Zéfling committed
267
    private parseColor(color: string): ColorNumber {
Zéfling's avatar
Zéfling committed
268
269
270
271
272
273
274
275

        // si named color
        if (Coloration.colorsName[color]) {
            color = Coloration.colorsName[color];
        }

        // validate hexa string #RGB, #RGBA, #RRGGBB, #RRGGBBAA (ignore alpha)
        const matchHex = String(color).match(pattern.hexa);
Zéfling's avatar
Zéfling committed
276
277
        let intColor: number;
        let alpha = 1;
Zéfling's avatar
Zéfling committed
278
        if (matchHex) {
Zéfling's avatar
Zéfling committed
279
            const hexaColor = matchHex[4]
Zéfling's avatar
Zéfling committed
280
281
                ? matchHex[2] + matchHex[4]
                : matchHex[2][0] + matchHex[2][0] + matchHex[2][1] + matchHex[2][1] + matchHex[2][2] + matchHex[2][2];
Zéfling's avatar
Zéfling committed
282
283
            const hexaAlpha = matchHex[4]
                ? matchHex[5]
Zéfling's avatar
Zéfling committed
284
                : (matchHex[3] ? matchHex[3][1] + matchHex[3][1] : undefined);
Zéfling's avatar
Zéfling committed
285
            intColor = parseInt(hexaColor, 16);
Zéfling's avatar
Zéfling committed
286
            alpha = hexaAlpha !== undefined ? parseInt(hexaAlpha, 16) / 255 : 1;
Zéfling's avatar
Zéfling committed
287
288
289
290
291
292
        }

        if (intColor === undefined) {
            // validate rgb() / rgba()
            const matchRgb = String(color).match(pattern.rgba);
            if (matchRgb) {
Zéfling's avatar
Zéfling committed
293
                intColor = this.rgbToInt(parseInt(matchRgb[1], 10), parseInt(matchRgb[4], 10), parseInt(matchRgb[7], 10));
Zéfling's avatar
Zéfling committed
294
                alpha = matchRgb[11] !== undefined ? parseFloat(matchRgb[11]) : 1;
Zéfling's avatar
Zéfling committed
295
296
297
298
299
300
301
            }
        }

        if (intColor === undefined) {
            // validate hsv() / hsva()
            const matchHsv = String(color).match(pattern.hsva);
            if (matchHsv) {
302
                intColor = this.hsvToInt(parseInt(matchHsv[1], 10), parseInt(matchHsv[4], 10), parseInt(matchHsv[7], 10));
Zéfling's avatar
Zéfling committed
303
                alpha = matchHsv[11] !== undefined ? parseFloat(matchHsv[11]) : 1;
Zéfling's avatar
Zéfling committed
304
305
306
            }
        }

Zéfling's avatar
Zéfling committed
307
        return { intColor, alpha };
Zéfling's avatar
Zéfling committed
308
309
310

    }

Zéfling's avatar
Zéfling committed
311
312
313
314
315
316
317
318
    /**
     * convert HSV/HSL to int RGB
     * @param hue Hue [0, 360]
     * @param saturation Saturation [0, 100]
     * @param value Value [0, 100]
     * @returns int RGB
     * @see https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
     */
319
    private hsvToInt(hue: number, saturation: number, value: number): number {
Zéfling's avatar
Zéfling committed
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
        saturation = Math.max(0, saturation);

        const s = saturation / 100;
        const v = value / 100;
        const h = hue / 360;

        if (saturation === 0) {
            return this.rgbToInt(v * 256, v * 256, v * 256);
        }

        const q = v < 0.5 ? v * (1 + s) : v + s - v * s;
        const p = 2 * v - q;
        const r = this.hue2rgb(p, q, h + 1 / 3);
        const g = this.hue2rgb(p, q, h);
        const b = this.hue2rgb(p, q, h - 1 / 3);

        return this.rgbToInt(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255));
    }

339
340
341
342
343
344
345
    /**
     * hue color calculation
     * @param p number
     * @param q number
     * @param t number
     * @returns int color [0, 255]
     */
Zéfling's avatar
Zéfling committed
346
347
348
349
350
351
352
353
    private hue2rgb(p: number, q: number, t: number): number {
        if (t < 0) { t += 1; } else if (t > 1) { t -= 1; }
        if (t < 1 / 6) { return p + (q - p) * 6 * t; }
        if (t < 1 / 2) { return q; }
        if (t < 2 / 3) { return p + (q - p) * (2 / 3 - t) * 6; }
        return p;
    }

354
355
    /**
     * Convert RGB to HSV
Zéfling's avatar
Zéfling committed
356
357
358
     * @param r red [0, 255]
     * @param g green [0, 255]
     * @param b blue [0, 255]
359
     * @returns HSV data
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
     * @see https://stackoverflow.com/questions/39118528/rgb-to-hsl-conversion
     * @see https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
     */
    private rgb2hsv(r: number, g: number, b: number): HSV {

        // convert r,g,b [0,255] range to [0,1]
        r = r / 255;
        g = g / 255;
        b = b / 255;

        // get the min and max of r,g,b
        const max = Math.max(r, g, b);
        const min = Math.min(r, g, b);

        // lightness is the average of the largest and smallest color components
        const val = (max + min) / 2;
        let hue: number;
        let sat: number;

        if (max === min) { // no saturation
            hue = 0;
            sat = 0;
        } else {
            const c = max - min; // chroma
            // saturation is simply the chroma scaled to fill
            // the interval [0, 1] for every combination of hue and lightness
            sat = c / (1 - Math.abs(2 * val - 1));
            switch (max) {
                case r:
                    hue = (g - b) / c + (g < b ? 6 : 0);
                    break;
                case g:
                    hue = (b - r) / c + 2;
                    break;
                case b:
                    hue = (r - g) / c + 4;
                    break;
            }
        }

        return {
            h: Math.round(hue * 60),  // °
            s: Math.round(sat * 100), // %
            v: Math.round(val * 100)  // %
        };
    }

407
408
409
410
411
412
413
    /**
     * convert RGB data to int value
     * @param r red [0, 255]
     * @param g green [0, 255]
     * @param b blue [0, 255]
     * @returns int value
     */
Zéfling's avatar
Zéfling committed
414
    private rgbToInt(r: number, g: number, b: number): number {
Zéfling's avatar
Zéfling committed
415
        return Math.round(r) * 0x10000 + Math.round(g) * 0x100 + Math.round(b);
Zéfling's avatar
Zéfling committed
416
417
    }

418
419
420
421
422
423
424
425
    /**
     * bound a value between two values
     * @param value value
     * @param min min value
     * @param max max value
     * @param defaultValue replace an invalid vvalue
     * @returns value between min and max
     */
426
427
    private minmax(value: number, min: number, max: number, defaultValue = 0) {
        return Math.min(Math.max(value || defaultValue, min), max);
Zéfling's avatar
Zéfling committed
428
429
    }

430
431
432
433
434
435
    /**
     * test is intColor is valid for return a vlaue
     * @param value callback of value
     * @returns value or null
     */
    private value<L>(value: () => L): L {
Zéfling's avatar
Zéfling committed
436
        return !isNaN(this.calcColor.intColor) ? value() : null;
437
438
    }

Zéfling's avatar
Zéfling committed
439
}