UNPKG

@fooloomanzoo/property-mixins

Version:

mixin for custom elements to extends property mixins for data formats

857 lines (779 loc) 25.7 kB
import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js'; import { safeAdd, safeMult, normalizedClamp, mathMod } from './number-utilities.js'; /** * @typedef {Object} rgbObject rgb object * @property {Number} r - The red part of the rgb color * @property {Number} g - The green part of the rgb color * @property {Number} b - The blue part of the rgb color */ /** * @typedef {Object} hslObject hsl object * @property {Number} h The hue part of the hsl color * @property {Number} s The saturation part of the hsl color * @property {Number} l The lightness part of the hsl color */ /** * generate random rgb-color * @return {rgbObject} rgb-color object */ export const randomRgb = function() { return { r: Math.round(255 * Math.random()), g: Math.round(255 * Math.random()), b: Math.round(255 * Math.random()) }; } /** * RegularExpression for parsing color strings by format */ export const regexpRgb = /^\s*rgb(a)?\(\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)(?:\s*,\s*(-?\d*(?:\.(?:\d*)?)?))?\s*\)\s*$/; export const regexpHsl = /^\s*hsl(a)?\(\s*(-?\d+(?:\.\d*)?)\s*,\s*(-?\d*(?:\.(?:\d*)?)?%?)\s*,\s*(-?\d*(?:\.(?:\d*)?)?%?)(?:\s*,\s*(-?\d*(?:\.(?:\d*)?)?))?\s*\)\s*$/; export const regexpHex = /^\s*(?:(#[A-Fa-f0-9]{6})([A-Fa-f0-9]{2})?|(#[A-Fa-f0-9]{3})([A-Fa-f0-9])?)\s*$/; export const regexpAuto = /^\s*(?:[\w]*|#[A-Fa-f0-9]{3,8}|rgba?\([\d,.\s]+\)|hsla?\([\d,.%\s]+\))\s*$/; /** * RegularExpression for percent */ export const regexpPercent = /(-?\d*(?:\.(?:\d*)?)?)%/; /** * normalize rgb properties * @param {rgbObject} rgb - The rgb-object of which the values should be normalized * @return {rgbObject} The normalized rgb-object. */ export const normalizeRgb = function(rgb) { rgb.r = mathMod(Math.round(rgb.r), 256); rgb.g = mathMod(Math.round(rgb.g), 256); rgb.b = mathMod(Math.round(rgb.b), 256); return rgb; } /** * normalize hsl values * @param {hslObject} hsl - The hsl-object of which the values should be normalized * @param {number} hslPrecision - The precision of the hsl properties * @return {hslObject} The normalized hsl-object. */ export const normalizeHsl = function(hsl, hslPrecision) { hslPrecision = hslPrecision || 0; hsl.h = mathMod(+(+hsl.h).toFixed(hslPrecision), 360); hsl.s = normalizedClamp(+(+hsl.s).toFixed(hslPrecision + 2)); hsl.l = normalizedClamp(+(+hsl.l).toFixed(hslPrecision + 2)); return hsl; } /** * convert hex to rgb * @param {string} hex The hex string without alpha * @return {rgbObject} the rgb object */ export const hexToRgb = function(hex) { if (hex === undefined) { return; } hex = hex.slice(1); if (hex.length === 3) { hex = hex.replace(/(.)(.)(.)/, "$1$1$2$2$3$3"); } return { r: parseInt(hex.substr(0, 2), 16), g: parseInt(hex.substr(2, 2), 16), b: parseInt(hex.substr(4, 2), 16) }; } /** * convert rgb component to partial hex-string * @param {number} component rgb-component * @return {string} hex-string */ const rgbComponentToHex = function(component) { const _hex = component.toString(16); if (_hex.length === 1) { return `0${_hex}`; } else { return _hex.slice(0, 2); } } /** * convert hex to rgb * @param {string} hex The hex string without alpha * @return {rgbObject} the rgb object */ export const rgbToHex = function(rgb) { if (isNaN(rgb.r) || isNaN(rgb.g) || isNaN(rgb.b)) { return; } return '#' + rgbComponentToHex(Math.round(rgb.r)) + rgbComponentToHex(Math.round(rgb.g)) + rgbComponentToHex(Math.round(rgb.b)); } /** * convert rgb to hsl (the values are not rounded) * @param {rgbObject} rgb rgb object * @param {Number} defaultH hue to set if saturation is 0 * @return {hslObject} hsl object */ export const rgbToHsl = function(rgb, defaultH) { if (isNaN(rgb.r) || isNaN(rgb.g) || isNaN(rgb.b)) { return; } const max = Math.max(rgb.r, rgb.g, rgb.b), min = Math.min(rgb.r, rgb.g, rgb.b); const l = (max + min) / (2 * 255); const _c = max - min; // not-normalized chroma (for precision) if (_c === 0) { return { h: defaultH || 0, s: 0, l: normalizedClamp(l) }; } const s = _c / (255 - Math.abs(max + min - 255)); let h; switch (max) { case rgb.r: h = ((rgb.g - rgb.b) * 60) / _c; break; case rgb.g: h = (((rgb.b - rgb.r) * 60) / _c) + 120; break; case rgb.b: h = (((rgb.r - rgb.g) * 60) / _c) + 240; break; } return { h: mathMod(h, 360), s: normalizedClamp(s), l: normalizedClamp(l) }; } /** * hsl component to rgb component, part of hsl to rgb algorithm * @param {number} t1 * @param {number} t2 * @param {number} t3 * @return {number} rgb component */ const hslComponentToRgbComponent = function(t1, t2, t3) { if (t3 < 0) t3 += 360; if (t3 >= 360) t3 -= 360; if (t3 < 60) return (((t2 - t1) * t3) / 60 + t1); else if (t3 < 180) return t2; else if (t3 < 240) return (((t2 - t1) * (240 - t3)) / 60 + t1); else return t1; } /** * convert hsl to rgb (the values are not rounded) * @param {hslObject} hsl hsl object * @return {rgbObject} rgb object */ export const hslToRgb = function(hsl) { // not rounded yet if (isNaN(hsl.h) || isNaN(hsl.s) || isNaN(hsl.l)) { return; } const t2 = ((hsl.l <= 0.5) ? hsl.l * (hsl.s + 1) : hsl.l + hsl.s - (hsl.l * hsl.s)) * 255, t1 = hsl.l * 2 * 255 - t2; return { r: mathMod(hslComponentToRgbComponent(t1, t2, hsl.h + 120), 256), g: mathMod(hslComponentToRgbComponent(t1, t2, hsl.h), 256), b: mathMod(hslComponentToRgbComponent(t1, t2, hsl.h - 120), 256) }; } /** * convert alpha number to hex-alpha-string * @param {number} alpha The alpha number * @param {number} length The expected string length * @return {string} The computed hex-alpha-string */ export const alphaToHex = function(alpha, length) { const base = Math.pow(16, length) - 1; let hex = (Math.round(alpha * base)).toString(16); while (length > hex.length) hex = '0' + hex; return hex; } /** * convert hex-alpha-string to alpha number * @param {string} hex The hex-alpha-string * @param {number} length The concidered string length * @return {number} The computed alpha number */ export const hexToAlpha = function(hex, length) { const base = Math.pow(16, length) - 1; // rounding, because browser do use for return safeMult(Math.round(100 * (parseInt(hex, 16) / base)), 0.01); } /** * compute rgb color properties by data from CanvasContext.getImageData() * @class imgData * @param {Uint8ClampedArray} data The image data created by CanvasContext.getImageData() * @property {number} r The red color rgb property * @property {number} g The green color rgb property * @property {number} b The blue color rgb property * @property {number} alpha The alpha property * @property {boolean} alphaMode True if alpha is unequal 1 */ function imgData(data) { return { r: data[0], g: data[1], b: data[2], alpha: hexToAlpha(data[3].toString(16), 2), alphaMode: data[3] !== 255 }; } /** * test a color string in local browser environment * @param {CanvasRenderingContext2D} canvasContext The canvas context to test on * @param {string} colorString The color string to tests * @return {imgData} The resulted color data of the test */ export const testColor = function(canvasContext, colorString) { // NOTE: if the color string is not valid, the fill of the canvas might not change canvasContext.clearRect(0, 0, 1, 1); canvasContext.fillStyle = colorString || '#000'; canvasContext.fillRect(0, 0, 1, 1); const data = canvasContext.getImageData(0, 0, 1, 1).data; canvasContext.clearRect(0, 0, 1, 1); return imgData(data) } /** * Mixin that provides web-color-properties. Its `color-string` converts automatically beetween different formats (hex, rgb and hsl) and provides an alpha-colorString. * * @mixinFunction * @polymer * * @demo demo/color-demo.html */ export const ColorMixin = dedupingMixin(superClass => { return class extends superClass { constructor() { super(); this._validFormats = ['rgb', 'hex', 'hsl', 'auto']; this.randomColor = this.randomColor.bind(this); this.resetColor = this.resetColor.bind(this); } static get properties() { return { /** * Hex-color */ hex: { type: String, notify: true, observer: '_hexChanged' }, /** * Red */ r: { type: Number, notify: true }, /** * Green */ g: { type: Number, notify: true }, /** * Blue */ b: { type: Number, notify: true }, /** * Hue */ h: { type: Number, notify: true }, /** * Saturation (hsl) */ s: { type: Number, notify: true }, /** * Lightness */ l: { type: Number, notify: true }, /** * Alpha */ alpha: { type: Number, notify: true, value: 1, observer: '_alphaChanged' }, /** * if true, colorString has alpha */ alphaMode: { type: Boolean, notify: true, observer: '_alphaModeChanged' }, /** * if true, alpha won't be used */ withoutAlpha: { type: Boolean, observer: '_withoutAlphaChanged' }, /** * Precision of hsl-colorStrings, if the format is 'hsl' (for saturation and lightness it is applied according to their percentage colorString) */ hslPrecision: { type: Number, value: 0 }, /** * format of the colorString (possible colorStrings: 'rgb', 'hex', 'hsl', 'auto') */ format: { type: String, notify: true, value: 'auto', observer: '_formatChanged' }, /** * format is locked and does not switch according to the colorString */ fixedFormat: { type: Boolean }, /** * if true, hex alpha is supported by the browser */ _hexAlphaSupported: { type: Boolean, readOnly: true }, /** * value as color-string */ colorString: { type: String, notify: true, observer: '_colorStringChanged' } } } static get observers() { return [ '_rgbChanged(r, g, b)', '_hslChanged(h, s, l)' ]; } ready() { super.ready(); this._createTestCanvas(); } /** * generate random color */ randomColor() { this.__updateByColorString = this.__updateByColorProperties = false; this.setProperties(randomRgb()); } /** * reset all color properties */ resetColor() { this.__updateByColorString = this.__updateByColorProperties = false; this.setProperties({ colorString: undefined, r: undefined, g: undefined, b: undefined, h: undefined, s: undefined, l: undefined, hex: undefined, alpha: undefined }) } /** * creates a canvas for testing a color string and browser capabilities */ _createTestCanvas() { const testcanvas = document.createElement('canvas'); testcanvas.width = 1; testcanvas.height = 1; testcanvas.style.visibility = 'hidden'; testcanvas.style.pointerEvents = 'none'; testcanvas.style.position = 'fixed'; this.appendChild(testcanvas); this._testCanvasContext = testcanvas.getContext("2d"); this._testCanvasContext.beginPath(); // test if `#rrggbbaa` is supported const rgba = testColor(this._testCanvasContext, '#00000000'); this._set_hexAlphaSupported(rgba.alpha === 0); } _colorStringChanged(colorString) { if (!colorString || typeof colorString !== 'string') { this.resetColor(); return; } let toSet = {}, match, format, fixedFormat = this.fixedFormat; // input-format detection // hsl-format if (match = colorString.match(regexpHsl)) { format = 'hsl'; if (this.withoutAlpha || match[1] === undefined) { // no alpha toSet.alpha = 1; toSet.alphaMode = false; } else { // with alpha toSet.alpha = normalizedClamp(+match[5]); toSet.alphaMode = true; } toSet.h = +match[2]; let matchPercent; if (matchPercent = match[3].match(regexpPercent)) { // s in percentage colorString toSet.s = safeMult(+matchPercent[1], 0.01); } else { toSet.s = +match[3]; } if (matchPercent = match[4].match(regexpPercent)) { // l in percentage colorString toSet.l = safeMult(+matchPercent[1], 0.01); } else { toSet.l = +match[4]; } toSet = normalizeHsl(toSet, this.hslPrecision); // rgb-format } else if (match = colorString.match(regexpRgb)) { format = 'rgb'; if (this.withoutAlpha || match[1] === undefined) { // no alpha toSet.alpha = 1; toSet.alphaMode = false; } else { // with alpha toSet.alpha = normalizedClamp(+match[5]); toSet.alphaMode = true; } Object.assign(toSet, normalizeRgb({ r: +match[2], g: +match[3], b: +match[4] })); // hex-format } else if (match = colorString.match(regexpHex)) { format = 'hex'; if (match[1] !== undefined) { // six hex numbers toSet.hex = match[1]; if (!this.withoutAlpha && match[2] !== undefined) { // alpha channel has two hex numbers toSet.alpha = hexToAlpha(match[2], 2); toSet.alphaMode = true; if (!this._hexAlphaSupported) { format = 'rgb'; fixedFormat = false; } } else { // no alpha channel toSet.alpha = 1; toSet.alphaMode = false; } } else if (match[3] !== undefined) { // three hex numbers toSet.hex = match[3]; if (!this.withoutAlpha && match[4] !== undefined) { // alpha channel has one hex number toSet.alpha = hexToAlpha(match[4], 1); toSet.alphaMode = true; if (!this._hexAlphaSupported) { format = 'rgb'; fixedFormat = false; } } else { // no alpha channel toSet.alpha = 1; toSet.alphaMode = false; } } } else { // last try format = 'auto'; if (!this._testCanvasContext) { this._createTestCanvas(); } toSet = testColor(this._testCanvasContext, colorString); } if (this.format !== format && this.format !== 'auto') { // don't automatically change the format, if the format is `auto` or `fixedFormat` is set if (!fixedFormat) { toSet.format = format; } } this.__updateByColorString = true; this.__updateByColorProperties = false; this.setProperties(toSet); } /** * compute color string * @param {rgbObject} rgb The rgb object * @param {hslObject} hsl The hsl object * @param {string} hex The hex string * @param {string} oldColor The old color string before setting * @return {[type]} The computed color string */ _computeColorString(rgb, hsl, hex, oldColor) { const alpha = isNaN(this.alpha) ? 1 : this.alpha; const alphaMode = !this.withoutAlpha && (this.alphaMode === true || alpha !== 1); let format = this.format; if (format === 'auto') { // define output format from oldColorstring or if a named color is set if (oldColor) { if (!this._testCanvasContext) { this._createTestCanvas(); } const testRgb = testColor(this._testCanvasContext, oldColor); const rgbIsNotSet = !rgb || isNaN(rgb.r) || isNaN(rgb.g) || isNaN(rgb.b); // test old color string if it changes in comparisson to the given color properties if (!alphaMode && (rgbIsNotSet || (rgb.r === testRgb.r && rgb.b === testRgb.b && rgb.g === testRgb.g))) { // keeping color string if possible return oldColor; } if (rgbIsNotSet) { // rgb might not been set yet rgb = testRgb; } if (oldColor.split('hsl').length > 1) { // oldColor is in hsl format format = 'hsl'; } else if (oldColor.split('rgb').length > 1) { // oldColor is in rgb format format = 'rgb'; } else { // fallback is hex format format = 'hex'; } } else { // fallback is hex format format = 'hex'; } } switch (format) { case 'hsl': const hslPrecision = this.hslPrecision || 0; if ((isNaN(hsl.h) || isNaN(hsl.s) || isNaN(hsl.l)) && !(isNaN(rgb.r) || isNaN(rgb.g) || isNaN(rgb.b))) { hsl = normalizeHsl(rgbToHsl(rgb, this.h)); } if (!(isNaN(hsl.h) || isNaN(hsl.s) || isNaN(hsl.l))) { if (alphaMode) { return `hsla(${hsl.h}, ${safeMult(hsl.s,100)}%, ${safeMult(hsl.l,100)}%, ${alpha})`; } else { return `hsl(${hsl.h}, ${safeMult(hsl.s,100)}%, ${safeMult(hsl.l,100)}%)`; } } // falls through case 'hex': if (!hex) { if ((isNaN(rgb.r) || isNaN(rgb.g) || isNaN(rgb.b)) && !(isNaN(hsl.h) || isNaN(hsl.s) || isNaN(hsl.l))) { rgb = normalizeRgb(hslToRgb(hsl)); } hex = rgbToHex(rgb); } if (alphaMode) { if (this._hexAlphaSupported) { return `${hex}${alphaToHex(alpha, hex.length <= 4 ? 1 : 2)}`; } // if hexAlphaSupported is not supported, fall through to rgb } else { return hex; } // falls through default: // fallback is rgb if ((isNaN(rgb.r) || isNaN(rgb.g) || isNaN(rgb.b)) && !(isNaN(hsl.h) || isNaN(hsl.s) || isNaN(hsl.l))) { rgb = normalizeRgb(hslToRgb(hsl)); } if (!(isNaN(rgb.r) || isNaN(rgb.g) || isNaN(rgb.b))) { if (alphaMode) { return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`; } else { return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; } } } } _hexChanged(hex) { if (!hex) { this.resetColor(); return; } const rgb = hexToRgb(hex); hex = rgbToHex(rgb); // only set hsl values by rgb to avoid conflicting changes if (hex !== this.hex) { this.hex = hex; return; } this.__updateByColorString = false; if (!(this.r === rgb.r && this.g === rgb.g && this.b === rgb.b)) { this.setProperties(rgb); } } _rgbChanged(r, g, b) { if (isNaN(r) && isNaN(g) && isNaN(b)) { this.resetColor(); return; } if (isNaN(r) || isNaN(g) || isNaN(b)) { // single property can be set without triggering property changes return; } const rgb = normalizeRgb({ r: r, g: g, b: b }); const hex = rgbToHex(rgb); // set rgb values again if they don't match their normalized version if (!(rgb.r === this.r && rgb.g === this.g && rgb.b === this.b)) { // transformed rgb is different than the current rgb, so set the rgb this.setProperties(rgb); return; } // avoid computed setting properties again when in '_hslChanged' properties where computed if (this.__updateByColorProperties === true) { this.__updateByColorProperties = false; return; } let toSet = {}; const hsl = normalizeHsl(rgbToHsl(rgb, this.h)); const colorString = this._computeColorString(rgb, hsl, hex, this.colorString); if (!(this.h === hsl.h && this.s === hsl.s && this.l === hsl.l)) { toSet = hsl; } if (hex !== this.hex) { toSet.hex = hex; } if (colorString !== this.colorString) { toSet.colorString = colorString; } this.__updateByColorString = false; this.__updateByColorProperties = true; this.setProperties(toSet); } _hslChanged(h, s, l) { if (isNaN(h) && isNaN(s) && isNaN(l)) { this.resetColor(); return; } if (isNaN(h) || isNaN(s) || isNaN(l)) { // single property can be set without triggering property changes return; } const hsl = normalizeHsl({ h: h, s: s, l: l }); // set hsl values again if they don't match their normalized version if (!(this.h === hsl.h && this.s === hsl.s && this.l === hsl.l)) { // transformed hsl is different than the current hsl, so set the hsl this.setProperties(hsl); return; } // avoid computed setting properties again when in '_hslChanged' properties where computed if (this.__updateByColorProperties === true) { this.__updateByColorProperties = false; return; } let toSet = {}; const rgb = normalizeRgb(hslToRgb(hsl)); const hex = rgbToHex(rgb); const colorString = this._computeColorString(rgb, hsl, hex, this.colorString); if (!(rgb.r === this.r && rgb.g === this.g && rgb.b === this.b)) { toSet = rgb; } if (hex !== this.hex) { toSet.hex = hex; } if (colorString !== this.colorString) { toSet.colorString = colorString; } this.__updateByColorString = false; this.__updateByColorProperties = true; this.setProperties(toSet); } _formatChanged(format, oldFormat) { if (this._validFormats.indexOf(format) === -1) { if (oldFormat && this._validFormats.indexOf(oldFormat) !== -1) { this.format = oldFormat; return; } this.format = 'auto'; return; } if (this.colorString) { const colorString = this._computeColorString({ r: this.r, g: this.g, b: this.b }, { h: this.h, s: this.s, l: this.l }, this.hex, this.colorString); if (colorString !== this.colorString) { this.colorString = colorString; } } } _alphaChanged(alpha, oldAlpha) { if (alpha === undefined) { return; } if (isNaN(alpha)) { this.alpha = 1; return; } alpha = normalizedClamp(alpha); if (alpha !== this.alpha) { this.alpha = alpha; return; } if (this.withoutAlpha && (alpha !== 1 || this.alphaMode)) { let toSet = {}; toSet.alpha = 1; toSet.alphaMode = false; this.setProperties(toSet); return; } if (!this.withoutAlpha && alpha !== 1 && !this.alphaMode) { this.alphaMode = true; return } if (this.colorString) { const colorString = this._computeColorString({r: this.r, g: this.g, b: this.b}, {h: this.h, s: this.s, l: this.l}, this.hex, this.colorString); if (colorString !== this.colorString) { this.colorString = colorString; } } } _alphaModeChanged(alphaMode) { if (alphaMode === undefined) { return; } if (this.withoutAlpha && (this.alpha !== 1 || alphaMode)) { let toSet = {}; toSet.alpha = 1; toSet.alphaMode = false; this.setProperties(toSet); return; } if (!alphaMode && this.alpha !== 1) { this.alpha = 1; return; } if (this.colorString) { const colorString = this._computeColorString({r: this.r, g: this.g, b: this.b}, {h: this.h, s: this.s, l: this.l}, this.hex, this.colorString); if (colorString !== this.colorString) { this.colorString = colorString; } } } _withoutAlphaChanged(withoutAlpha) { if (withoutAlpha === undefined) { return; } if (withoutAlpha === true && (this.alpha !== 1 || this.alphaMode)) { let toSet = {}; toSet.alpha = 1; toSet.alphaMode = false; this.setProperties(toSet); } } } });