UNPKG

javascript-color

Version:

Методы для работы с цветом и его компонентами

1,138 lines (1,010 loc) 34 kB
/** @description Работа с цветом [es6] * @author xaota * @types * * {integer} <-> {number} - целые числа * * {natural} <-> {number} - натуральные числа и ноль, т.е., {unsigned int} // ноль не натуральное число * * {byte} <-> {number} - целые числа [0..255] (включительно) * * {percent} <-> {number} - вещественное число [0..1] (включительно), как правило, получается из {byte} / 255 * @todo Ещё гора чего не описана. +этим тегом помечены кандидаты на оптимизацию, переписывание и т. д. * @feature Цепочные вызовы, типа `Color.fromRGB(0, 0, 255).saturate(.2).fade(0.3)` */ /** @section @imports */ import Num from 'javascript-std-lib/number.js'; import Vector from 'javascript-algebra/Vector.js'; import Canvas from 'javascript-canvas/Canvas.js'; import Blend from './library/Blend.js'; /** @section data */ const css = { // цвета CSS aliceblue : '#f0f8ff', antiquewhite : '#faebd7', aqua : '#00ffff', aquamarine : '#7fffd4', azure : '#f0ffff', beige : '#f5f5dc', bisque : '#ffe4c4', black : '#000', blanchedalmond : '#ffebcd', blue : '#00f', blueviolet : '#8a2be2', brown : '#a52a2a', burlywood : '#deb887', cadetblue : '#5f9ea0', chartreuse : '#7fff00', chocolate : '#d2691e', coral : '#ff7f50', cornflowerblue : '#6495ed', cornsilk : '#fff8dc', crimson : '#dc143c', cyan : '#0ff', darkblue : '#00008b', darkcyan : '#008b8b', darkgoldenrod : '#b8860b', darkgray : '#a9a9a9', darkgrey : '#a9a9a9', darkgreen : '#006400', 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', fuchsia : '#f0f', gainsboro : '#dcdcdc', ghostwhite : '#f8f8ff', gold : '#ffd700', goldenrod : '#daa520', gray : '#808080', grey : '#808080', green : '#008000', greenyellow : '#adff2f', 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', lightgrey : '#d3d3d3', lightgreen : '#90ee90', lightpink : '#ffb6c1', lightsalmon : '#ffa07a', lightseagreen : '#20b2aa', lightskyblue : '#87cefa', lightslategray : '#778899', lightslategrey : '#778899', lightsteelblue : '#b0c4de', lightyellow : '#ffffe0', lime : '#0f0', limegreen : '#32cd32', linen : '#faf0e6', magenta : '#f0f', maroon : '#800000', mediumaquamarine : '#66cdaa', mediumblue : '#0000cd', mediumorchid : '#ba55d3', mediumpurple : '#9370d8', mediumseagreen : '#3cb371', mediumslateblue : '#7b68ee', mediumspringgreen : '#00fa9a', mediumturquoise : '#48d1cc', mediumvioletred : '#c71585', midnightblue : '#191970', mintcream : '#f5fffa', mistyrose : '#ffe4e1', moccasin : '#ffe4b5', navajowhite : '#ffdead', navy : '#000080', oldlace : '#fdf5e6', olive : '#808000', olivedrab : '#6b8e23', orange : '#ffa500', orangered : '#ff4500', orchid : '#da70d6', palegoldenrod : '#eee8aa', palegreen : '#98fb98', paleturquoise : '#afeeee', palevioletred : '#d87093', papayawhip : '#ffefd5', peachpuff : '#ffdab9', peru : '#cd853f', pink : '#ffc0cb', plum : '#dda0dd', powderblue : '#b0e0e6', purple : '#800080', rebeccapurple : '#639', red : '#f00', rosybrown : '#bc8f8f', royalblue : '#4169e1', saddlebrown : '#8b4513', salmon : '#fa8072', sandybrown : '#f4a460', seagreen : '#2e8b57', seashell : '#fff5ee', sienna : '#a0522d', silver : '#c0c0c0', skyblue : '#87ceeb', slateblue : '#6a5acd', slategray : '#708090', slategrey : '#708090', snow : '#fffafa', springgreen : '#00ff7f', steelblue : '#4682b4', tan : '#d2b48c', teal : '#008080', thistle : '#d8bfd8', tomato : '#ff6347', turquoise : '#40e0d0', violet : '#ee82ee', wheat : '#f5deb3', white : '#fff', whitesmoke : '#f5f5f5', yellow : '#ff0', yellowgreen : '#9acd32' }; /** @section */ export default class Color { /** {Color} @constructor * @param {...percent} rgb значения компонент цвета * @param {percent} alpha уровень прозрачности цвета * @param {string} value название цвета (CSS, если есть) */ constructor(rgb, alpha = 1, value = undefined) { const [red, green, blue] = rgb; Object.assign(this, { channel: { red, green, blue, alpha }, value }); } /** Строковое представление цвета @debug * @return {string} "[Color CSS-syntax]" */ toString() { return `[Color ${this.css}]`; } /** Копирование цвета * @return {Color} объект копия цвета */ copy() { const rgb = this.rgb; return new Color([rgb.r, rgb.g, rgb.b], rgb.a, this.value); } /** @section Компоненты цвета */ get red() { return this.channel.red; } /** */ get green() { return this.channel.green; } /** */ get blue() { return this.channel.blue; } /** */ get alpha() { return this.channel.alpha; } /** @section Представление цвета в различных цветовых моделях */ get rgb() { return { r: this.red, g: this.green, b: this.blue, a: this.alpha } } /** */ get RGB() { const rgb = this.rgb; const a = rgb.a; const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => Math.round(Number(c.toFixed(5)) * 255)); return { r, g, b, a }; } /** */ get hex() { const rgb = this.RGB; const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => c.toString(16)); return { r, g, b }; } /** */ get HEX() { const hex = this.hex; let [r, g, b] = [hex.r, hex.g, hex.b].map(c => c.length === 2 ? c : '0' + c); if (r[0] === r[1] && g[0] === g[1] && b[0] === b[1]) [r, g, b] = [r[0], g[0], b[0]]; return (r + g + b).toLowerCase(); } /** */ get ahex() { const rgb = this.RGB; const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => c.toString(16)); const a = Math.round(rgb.a * 255).toString(16); return { r, g, b, a }; } /** */ get AHEX() { const hex = this.ahex; const [a, r, g, b] = [hex.a, hex.r, hex.g, hex.b].map(c => c.length === 2 ? c : '0' + c); return (a + r + g + b).toLowerCase(); } /** */ get HEXA() { const hex = this.ahex; const [a, r, g, b] = [hex.a, hex.r, hex.g, hex.b].map(c => c.length === 2 ? c : '0' + c); return (r + g + b + a).toLowerCase(); } /** */ get hsl() { const rgba = this.rgb; const [r, g, b, a] = [rgba.r, rgba.g, rgba.b, rgba.a]; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const l = (max + min) / 2; const d = max - min; let h = 0; let s = 0; if (max === min) return { h, s, l, a }; switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h = Math.round(h * 60); s = l > 0.5 ? d / (2 - max - min) : d / (max + min); return { h, s, l, a }; } /** */ get HSL() { const hsl = this.hsl; hsl.s = Math.round(hsl.s * 100); hsl.l = Math.round(hsl.l * 100); return hsl; } /** */ get hsv() { const rgba = this.rgb; const [r, g, b, a] = [rgba.r, rgba.g, rgba.b, rgba.a]; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const v = max; const d = max - min; const s = max === 0 ? 0 : d / max; let h = 0; if (max === min) return { h, s, v, a } switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h = Math.round(h * 60); return { h, s, v, a } } /** */ get HSV() { const hsv = this.hsv; hsv.s = Math.round(hsv.s * 100); hsv.v = Math.round(hsv.v * 100); return hsv; } /** */ get hsb() { const hsv = this.hsv; const brightness = hsv.v; delete hsv.v; hsv.b = brightness; return hsv; } /** */ get HSB() { const hsb = this.hsb; hsb.s = Math.round(hsb.s * 100); hsb.b = Math.round(hsb.b * 100); return hsb; } /** Цвет словом-названием @css @slow * @return {string} название цвета */ get key() { const hex = this.cssHEX; for (const i in css) if (css[i] === hex) return i; return undefined; } /** @subsection Представление цвета в различных цветовых моделях @css */ get css() { return this.value ? this.key : this.channel.alpha === 1 ? this.cssHEX : this.cssRGBA; } /** */ get cssRGB() { const rgb = this.RGB; return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; } /** */ get cssRGBA() { const rgba = this.RGB; return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`; } /** */ get cssHEX() { return '#' + this.HEX; } /** */ get cssHEXA() { // RGBA => #rrggbbaa return '#' + this.HEXA; } /** */ get cssAHEX() { // ARGB => #aarrggbb, not support in css return '#' + this.AHEX; } /** */ get cssHSL() { const hsl = this.HSL; return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`; } /** */ get cssHSLA() { const hsl = this.HSL; return `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${hsl.a})`; } /** */ get cssHSV() { // not support in css const hsv = this.HSV; return `hsv(${hsv.h}, ${hsv.s}%, ${hsv.v}%)`; } /** */ get cssHSVA() { // not support in css const hsv = this.HSV; return `hsva(${hsv.h}, ${hsv.s}%, ${hsv.v}%, ${hsv.a})`; } /** */ get cssHSB() { // css4 const hsb = this.HSB; return `hsb(${hsb.h}, ${hsb.s}%, ${hsb.b}%)`; } /** */ get cssHSBA() { // css4 const hsb = this.HSB; return `hsva(${hsb.h}, ${hsb.s}%, ${hsb.b}%, ${hsb.a})`; } /** @TODO: * HWB * CMYK * NCol * LAB * XYZ * LCH * LUV */ /** @section Характеристики цвета */ /** Яркость цвета * @return {float} [0..1] */ get luminance() { const rgb = this.rgb; const [r, g, b] = [rgb.r, rgb.g, rgb.b]; return luminance(r, g, b); } /** Относительная яркость цвета (relative luminance) * @standart WCAG 2.0 * @link //www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef * @return {float} [0..1] */ get luma() { const rgb = this.rgb; let [r, g, b] = [rgb.r, rgb.g, rgb.b]; r = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4); g = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4); b = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4); return luminance(r, g, b); } /** Светлота цвета * @return {float} [0..1] */ get lightness() { return this.hsl.l; } /** Насыщенность цвета @hsl @primary * @return {float} [0..1] */ get saturation() { return this.hsl.s; } /** Насыщенность цвета @hsv * @return {float} [0..1] */ get hsvSaturation() { return this.hsv.s; } /** Цветовой тон @hsl @primary @angle * @return {integer} [0..360] * @fixme: {float} [0..2 * Math.PI] - в радианах */ get hue() { return this.hsl.h; } /** Цветовой тон @hsv @angle * @return {integer} [0..360] * @fixme: {float} [0..2 * Math.PI] - в радианах */ get hsvHue() { return this.hsv.h; } /** Яркость (Значение цвета) @hsb / @hsv (value) * @return {float} [0..1] */ get brightness() { return this.hsv.v; // this.hsb.b } /** @TODO: * brightness (0..100?) * chroma (?) * tone ({string}) * heat (теплота) (?) */ /** @section Преобразование цвета @copy @chainable */ /** Изменение насыщенности * @param {percent} amount уровень * @param {boolean} relative?false использовать относительную насыщенность для расчёта * @return {Color} итоговый цвет * @todo: methods for relative calc - убрать условие */ saturate(amount, relative = false) { const saturation = this.saturation; amount = relative ? saturation * amount : amount; return this.SATURATION(saturation + amount); } /** */ desaturate(amount, relative = false) { // @deprecated return this.saturate(-amount, relative); } /** */ SATURATION(amount) { // @absolute const hsl = this.hsl; hsl.s = Num.clamp(amount); return Color.hsl(hsl); } /** Изменение яркости * @param {percent} amount устанавливаемое значениие * @param {boolean} relative?false использовать относительную светлоту для расчёта * @return {Color} итоговый цвет * @todo: methods for relative calc - убрать условие */ lighten(amount, relative = false) { const lightness = this.lightness; amount = relative ? lightness * amount : amount; return this.LIGHTNESS(lightness + amount); } /** */ darken(amount, relative = false) { return this.lighten(-amount, relative); } /** */ LIGHTNESS(amount) { // @absolute const hsl = this.hsl; hsl.l = Num.clamp(amount); return Color.hsl(hsl); } /** Изменение прозрачности * @param {percent} amount устанавливаемое значениие * @param {boolean} relative?false использовать относительную прозрачность как опорную * @return {Color} итоговый цвет * @todo: absolute method */ fade(amount, relative = false) { const alpha = this.alpha; amount = relative ? alpha * amount : amount; return this.FADE(alpha + amount); } /** */ fadeIn(amount, relative = false) { // @deprecated return this.fade(amount, relative); } /** */ fadeOut(amount, relative = false) { // @deprecated return this.fade(-amount, relative); } /** */ FADE(amount) { // @absolute const rgb = this.rgb; rgb.a = Num.clamp(amount); return Color.rgb(rgb); } /** Изменение цветового тона @hsl * @param {percent} amount устанавливаемое значениие * @return {Color} итоговый цвет * @todo: absolute method */ spin(amount) { const hsl = this.hsl; const hue = (hsl.h + amount) % 360; hsl.h = hue < 0 ? 360 + hue : hue; return Color.hsl(hsl); } /** Осветление (смешивание с белым) * @param {percent} amount устанавливаемое значениие * @return {Color} итоговый цвет */ tint(amount) { return Color.mix(Color.white, this, amount); } /** Затемнение (смешивание с чёрным) * @param {percent} amount устанавливаемое значениие * @return {Color} итоговый цвет */ shade(amount) { return Color.mix(Color.black, this, amount); } /** Преобразование в "Оттенки серого" на основе насыщенности // desaturate(100%) * @return {Color} итоговый цвет */ greyScale() { const hsl = this.hsl; hsl.s = 0; return Color.hsl(hsl); } /** Преобразование в "Оттенки серого" на основе интенсивности (яркости) * @return {Color} итоговый цвет */ lumaScale(relative = true) { const luma = relative === true ? this.luma : this.luminance; return new Color([luma, luma, luma], this.alpha); } /** Выбор наиболее контрастного цвета * @return {Color} итоговый цвет */ contrast(dark = Color.black, light = Color.white, threshold = 0.43) { light = light.copy(); dark = dark.copy(); if (dark.luma > light.luma) [light, dark] = [dark, light]; return this.luma < threshold ? light : dark; } /** Обратный (по модели RGB) цвет * @return {Color} итоговый цвет */ inverse() { // negative const c = this.rgb; const rgb = [c.r, c.g, c.b].map(e => 1 - e); return new Color(rgb, c.a); } /** Обратный (по модели RGB) цвет с прозрачностью * @return {Color} итоговый цвет */ INVERSE() { // negative const c = this.rgb; const rgb = [c.r, c.g, c.b].map(e => 1 - e); const a = 1 - c.a; return new Color(rgb, a); } /** Сложение цветов * @param {Color} color цвет-слагаемое * @return {Color} итоговый цвет * @todo: static (?), validate */ addition(color) { const channel = this.channel.map((c, i) => Num.clamp(c + color.channel[i])); const rgb = channel.slice(0, 3); const alpha = channel[3]; return new Color(rgb, alpha); } /** @TODO: * теплее/холоднее * изменение яркости (tint/shade (?)) */ /** @section Использование цвета для вычислений @native */ /** Вектор-компонент цвета @integer * @return {Int8Array}[0..2] - [R, G, B] компоненты цвета [0..255] */ get int() { const rgb = this.RGB; return new Int8Array([rgb.r, rgb.g, rgb.b]); } /** */ get INT() { const rgb = this.RGB; return new Int8Array([rgb.r, rgb.g, rgb.b, rgb.a]); } /** Вектор-компонент цвета @float * @return {Float32Array}[0..2] - [R, G, B] компоненты цвета [0..1] */ get float() { const rgb = this.rgb; return new Float32Array([rgb.r, rgb.g, rgb.b]); } /** */ get FLOAT() { const rgb = this.rgb; return new Float32Array([rgb.r, rgb.g, rgb.b, rgb.a]); } /** @section Комбинации цветов @static @method's */ /** Смешивание цветов * @param {Color} A цвет-компонент * @param {Color} B цвет-компонент * @param {percent} weight уровень смешивания * @return {Color} итоговый цвет */ static mix(A, B, weight = 0.5) { const w = weight * 2 - 1; const a = A.alpha - B.alpha; const w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; const w2 = 1 - w1; const r = w1 * A.red + w2 * B.red; const g = w1 * A.green + w2 * B.green; const b = w1 * A.blue + w2 * B.blue; const alpha = A.alpha * weight + B.alpha * (1 - weight); return new Color([r, g, b], alpha); } /** color interpolation */ static fade(A = Color.white, B = Color.black, percent = 0) { // interpolate const total = 100; const step = ~~(percent * 100); const channel = (a, b) => b + (~~((a - b) / total) * step); const r = channel(A.red, B.red); const g = channel(A.green, B.green); const b = channel(A.blue, B.blue); const alpha = channel(A.alpha * 255, B.alpha * 255) / 255; return new Color([r, g, b], alpha); } /** */ static fadeHSL(A = Color.white, B = Color.black, percent = 0) { } /** @subsection Наложение цветов */ static blend(mode, A = Color.white, B = Color.black) { const alphaA = A.alpha; const alphaB = B.alpha; const alpha = alphaB + alphaA * (1 - alphaB); const rgbA = A.float; const rgbB = B.float; const rgb = rgbA.map(blend); return new Color(rgb, alpha); /** */ function blend(a, i) { const b = rgbB[i]; const r = mode(a, b); return alpha !== 0 ? (alphaB * b + alphaA * (a - alphaB * (a + b - r))) / alpha : r; } } /** */ static multiply(A, B) { return Color.blend(Blend.multiply, A, B); } /** */ static screen(A, B) { return Color.blend(Blend.screen, A, B); } /** */ static overlay(A, B) { return Color.blend(Blend.overlay, A, B); } /** */ static softlight(A, B) { return Color.blend(Blend.softlight, A, B); } /** */ static hardlight(A, B) { return Color.blend(Blend.hardlight, A, B); } /** */ static difference(A, B) { return Color.blend(Blend.difference, A, B); } /** */ static exclusion(A, B) { return Color.blend(Blend.exclusion, A, B); } /** */ static average(A, B) { return Color.blend(Blend.average, A, B); } /** */ static negation(A, B) { return Color.blend(Blend.negation, A, B); } /** @subsection Создание цвета @static @method's */ static hex(rgb, value) { if (rgb.length === 3) rgb = rgb.split('').map(c => c + c).join(''); rgb = rgb.match(/.{2}/g).map(c => parseInt(c, 16)); return Color.fromRGB(...rgb, 1, value); } /** */ static ahex(argb, value) { if (argb.length === 4) argb = argb.split('').map(c => c + c).join(''); argb = argb.match(/.{2}/g).map(c => parseInt(c, 16)); const rgb = argb.slice(1); const alpha = argb[0] / 255; return Color.fromRGB(...rgb, alpha, value); } /** */ static rgb({ r, g, b, a = 1 }, value) { return new Color([r, g, b], a, value); } /** */ static RGB(rgba, value) { const [r, g, b] = [rgba.r, rgba.g, rgba.b].map(c => c / 255); return Color.rgb({ r, g, b, a: rgba.a }, value); } /** */ static hsl({ h, s, l, a = 1 }, value) { h = (h % 360) / 360; // s = Num.clamp(s); // l = Num.clamp(l); // a = Num.clamp(a); if (s === 0) return new Color([l, l, l], a); // achromatic const q = l <= 0.5 ? l * (s + 1) : l + s - l * s; const p = l * 2 - q; const r = hue(p, q, h + 1 / 3); const g = hue(p, q, h); const b = hue(p, q, h - 1 / 3); return new Color([r, g, b], a, value); } /** */ static HSL(hsla, value) { hsla = Object.assign({}, hsla, { s: hsla.s / 100, l: hsla.l / 100 }); return Color.hsl(hsla, value); } /** */ static hsv({ h, s, v, a = 1 }, value) { h %= 360; const i = Math.floor((h / 60) % 6); const f = (h / 60) - i; const vs = [ v, v * (1 - s), v * (1 - f * s), v * (1 - (1 - f) * s) ]; const perm = [ [0, 3, 1], [2, 0, 1], [1, 0, 3], [1, 2, 0], [3, 1, 0], [0, 1, 2] ]; const rgb = [ vs[perm[i][0]], vs[perm[i][1]], vs[perm[i][2]] ]; return new Color(rgb, a, value); } /** */ static HSV(hsva, value) { hsva = Object.assign({}, hsva, { s: hsva.s / 100, v: hsva.v / 100 }); return Color.hsv(hsva, value); } /** */ static hsb({ h, s, b, a = 1 }, value) { return Color.hsv({ h, s, v: b, a }, value); } /** */ static HSB(hsba, value) { hsba.v = hsba.b; return Color.HSV(hsba, value); } /** */ static fromRGB(r, g, b, a, value) { return Color.RGB({ r, g, b, a }, value); } /** */ static fromHEX(hex, value) { return hex[0] === '#' ? Color.hex(hex.slice(1), value) : Color.hex(hex, value); } /** */ static fromAHEX(ahex, value) { return ahex[0] === '#' ? Color.ahex(ahex.slice(1), value) : Color.ahex(ahex, value); } /** */ static fromHSL(h, s, l, a, value) { return Color.HSL({ h, s, l, a }, value); } /** */ static fromHSV(h, s, v, a, value) { return Color.HSV({ h, s, v, a }, value); } /** */ static fromHSB(h, s, b, a, value) { return Color.HSB({ h, s, b, a }, value); } /** @subssection Создание цвета из параметра @css */ static cssRGB(color) { // rgb({byte}, {byte}, {byte}) или rgba({byte}, {byte}, {byte}, {percent}) color = color.replace(/rgb|rgba|\(|\)\s+/gi, '').split(',').map(Number); const r = color[0]; const g = color[1]; const b = color[2]; const a = color.length === 4 ? color[3] : 1; return Color.fromRGB(r, g, b, a); } /** */ static cssHEX(color) { return Color.fromHEX(color); } /** */ static cssAHEX(color) { return Color.fromAHEX(color); } /** */ static cssHSL(color) { color = color.replace(/hsl|hsla|\(|\)|\%|\s+/gi, '').split(',').map(Number); const h = color[0]; const s = color[1]; const l = color[2]; const a = color.length === 4 ? color[3] : 1; return Color.fromHSL(h, s, l, a); } /** */ static cssHSV(color) { color = color.replace(/hsv|hsva|\(|\)|\%|\s+/gi, '').split(',').map(Number); const h = color[0]; const s = color[1]; const v = color[2]; const a = color.length === 4 ? color[3] : 1; return Color.fromHSV(h, s, v, a); } /** */ static cssHSB(color) { color = color.replace(/hsb|hsba|\(|\)|\%|\s+/gi, '').split(',').map(Number); const h = color[0]; const s = color[1]; const b = color[2]; const a = color.length === 4 ? color[3] : 1; return Color.fromHSB(h, s, b, a); } /** */ static key(color) { return color in css ? Color[color].copy() : undefined; } /** */ static css(color) { if (color.charAt(0) === '#') return Color.cssHEX(color); switch (color.substr(0, 3)) { case 'rgb': return Color.cssRGB(color); case 'hsl': return Color.cssHSL(color); case 'hsv': return Color.cssHSV(color); case 'hsb': return Color.cssHSB(color); } return Color.key(color); } /** @subsection Сравнение цветов */ static compare(A, B, epsilon) { A = A.float, B = B.float; return A.every((e, i) => Math.abs(e - B[i]) < epsilon); } /** */ static COMPARE(A, B, epsilon) { A = A.FLOAT, B = B.FLOAT; return A.every((e, i) => Math.abs(e - B[i]) < epsilon); } /** @section Списки цветов */ static listFromRGBA(...array) { let r, g, b, a; return Array.from({ length: array.length }, function(_, index) { index *= 4; r = array[index]; g = array[index + 1]; b = array[index + 2]; a = array[index + 3]; return Color.fromRGB(r, g, b, a); }); } /** Список цветов из изображения (по пикселям) * @param {Image} image Изображение * @param {Vector} size? Размер изображения * @return {array} {...Color} */ static listFromImage(image, size = Vector.from(image.width, image.height)) { const canvas = new Canvas().view(size); const pixels = canvas.image(image).pixels(); return Color.listFromRGBA(...pixels.data); } /** Список уникальных цветов из изображения (по пикселям) @slow (O(n^2)) * @param {Image} image Изображение * @param {Vector} size? Размер изображения * @param {number} epsilon точность * @return {array} {...Color} */ static uniqListFromImage(image, size, epsilon) { const list = Color.listFromImage(image, size); const result = []; for (let index = 0; index < list.length; index++) { const color = list[index]; if (result.some(e => Color.compare(e, color, epsilon))) continue; result.push(color); } return result; } /** Средний цвет из изображения */ static averageFromImage(image, options) { const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = image.width; canvas.height = image.height; context.clearRect(0, 0, image.width, image.height); context.drawImage(image, 0, 0, image.width, image.height); const bitmap = context.getImageData(0, 0, image.width, image.height).data; const value = getColorFromArray4(bitmap, options); return prepareResult(value); } /** [r,g,b] */ static isDark(color) { // http://www.w3.org/TR/AERT#color-contrast return ((color[0] * 299 + color[1] * 587 + color[2] * 114) / 1000) < 128; } } /** @section Инициализация основных цветов @css */ Color.transparent = new Color([0, 0, 0], 0, 'transparent'); for (const color in css) Color[color] = Color.hex(css[color].slice(1), color); // #region [Private] /** @subsection common.color */ /** Перевод цветового тона в компоненты @rgb */ function hue(p, q, h) { h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h); if (h * 6 < 1) return p + (q - p) * h * 6; if (h * 2 < 1) return q; if (h * 3 < 2) return p + (q - p) * (2 / 3 - h) * 6; return p; } /** Яркость цвета */ function luminance(r, g, b) { return 0.2126 * r + 0.7152 * g + 0.0722 * b; } /** */ function getColorFromArray4(arr, options = {}) { const bytesPerPixel = 4; const arrLength = arr.length; const len = arrLength - arrLength % bytesPerPixel; const preparedStep = (options.step || 1) * bytesPerPixel; const algorithm = sqrtAlgorithm; return algorithm(arr, len, preparedStep); } /** */ function sqrtAlgorithm(arr, len, preparedStep) { let redTotal = 0; let greenTotal = 0; let blueTotal = 0; let alphaTotal = 0; let count = 0; for (let i = 0; i < len; i += preparedStep) { const red = arr[i]; const green = arr[i + 1]; const blue = arr[i + 2]; const alpha = arr[i + 3]; redTotal += red * red * alpha; greenTotal += green * green * alpha; blueTotal += blue * blue * alpha; alphaTotal += alpha; count++; } return alphaTotal ? [ Math.round(Math.sqrt(redTotal / alphaTotal)), Math.round(Math.sqrt(greenTotal / alphaTotal)), Math.round(Math.sqrt(blueTotal / alphaTotal)), Math.round(alphaTotal / count) ] : [0, 0, 0, 0]; } /** */ function prepareResult(value) { const rgb = value.slice(0, 3); const rgba = [].concat(rgb, value[3] / 255); const isDark = Color.isDark(value); return { value, rgb: 'rgb(' + rgb.join(',') + ')', rgba: 'rgba(' + rgba.join(',') + ')', isDark, isLight: !isDark }; } // #endregion