UNPKG

hex-to-css-filter

Version:

hex-to-css-filter - Easy way to generate colors from HEX to CSS Filters

545 lines (533 loc) 20.9 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.HexToCSSFilter = {})); })(this, (function (exports) { 'use strict'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __read(o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var Color = /** @class */ (function () { function Color(r, g, b) { this.r = 0; this.g = 0; this.b = 0; this.set(r, g, b); } Color.prototype.set = function (r, g, b) { this.r = this.clamp(r); this.g = this.clamp(g); this.b = this.clamp(b); }; /** * Applying cals to get CSS filter for hue-rotate * * @param {number} [angle=0] * @memberof Color */ Color.prototype.hueRotate = function (angle) { if (angle === void 0) { angle = 0; } angle = (angle / 180) * Math.PI; var sin = Math.sin(angle); var cos = Math.cos(angle); this.multiply([ 0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928, 0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.14, 0.072 - cos * 0.072 - sin * 0.283, 0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072, ]); }; /** * Applying cals to get CSS filter for grayscale * * @param {number} [value=1] * @memberof Color */ Color.prototype.grayscale = function (value) { if (value === void 0) { value = 1; } this.multiply([ 0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value), 0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value), 0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value), ]); }; /** * Applying cals to get CSS filter for sepia * * @param {number} [value=1] * @memberof Color */ Color.prototype.sepia = function (value) { if (value === void 0) { value = 1; } this.multiply([ 0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value), 0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value), 0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value), ]); }; /** * Applying cals to get CSS filter for saturate * * @param {number} [value=1] * @memberof Color */ Color.prototype.saturate = function (value) { if (value === void 0) { value = 1; } this.multiply([ 0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value, 0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value, 0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value, ]); }; Color.prototype.multiply = function (matrix) { // These values are needed. It's correct because the returned values will change var newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]); var newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]); var newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]); this.r = newR; this.g = newG; this.b = newB; }; /** * Applying cals to get CSS filter for brightness * * @param {number} [value=1] * @memberof Color */ Color.prototype.brightness = function (value) { if (value === void 0) { value = 1; } this.linear(value); }; /** * Applying cals to get CSS filter for contrast * * @param {number} [value=1] * @memberof Color */ Color.prototype.contrast = function (value) { if (value === void 0) { value = 1; } this.linear(value, -(0.5 * value) + 0.5); }; Color.prototype.linear = function (slope, intercept) { if (slope === void 0) { slope = 1; } if (intercept === void 0) { intercept = 0; } this.r = this.clamp(this.r * slope + intercept * 255); this.g = this.clamp(this.g * slope + intercept * 255); this.b = this.clamp(this.b * slope + intercept * 255); }; /** * Applying cals to get CSS filter for invert * * @param {number} [value=1] * @memberof Color */ Color.prototype.invert = function (value) { if (value === void 0) { value = 1; } this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255); this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255); this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255); }; /** * transform RGB into HSL values * * @returns {HSLData} * @memberof Color */ Color.prototype.hsl = function () { var red = this.r / 255; var green = this.g / 255; var blue = this.b / 255; // find greatest and smallest channel values var max = Math.max(red, green, blue); var min = Math.min(red, green, blue); var hue = 0; var saturation = 0; var lightness = (max + min) / 2; // If min and max have the same values, it means // the given color is achromatic if (max === min) { return { h: 0, s: 0, l: lightness * 100, }; } // Adding delta value of greatest and smallest channel values var delta = max - min; saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min); if (max === red) { hue = (green - blue) / delta + (green < blue ? 6 : 0); } else if (max === green) { hue = (blue - red) / delta + 2; } else if (max === blue) { hue = (red - green) / delta + 4; } hue /= 6; return { h: hue * 100, s: saturation * 100, l: lightness * 100, }; }; /** * Normalize the value to follow the min and max for RGB colors * min: 0 * max: 255 * * @private * @param {number} value * @returns {number} * @memberof Color */ Color.prototype.clamp = function (value) { // Minimum RGB Value = 0; // Maximum RGB Value = 255; return Math.min(Math.max(value, 0), 255); }; return Color; }()); var Solver = /** @class */ (function () { function Solver(target, options) { this.target = target; this.targetHSL = target.hsl(); this.options = Object.assign({}, // Adding default values for options { acceptanceLossPercentage: 5, maxChecks: 15, }, options); // All the calcs done by the library to generate // a CSS Filter are based on the color `#000` // in this case, `rgb(0, 0, 0)` // Please make sure the background of the element // is `#000` for better performance // and color similarity. this.reusedColor = new Color(0, 0, 0); } /** * Returns the solved values for the * * @returns {(SPSAPayload & { filter: string; })} * @memberof Solver */ Solver.prototype.solve = function () { var result = this.solveNarrow(this.solveWide()); return { values: result.values, called: result.called, loss: result.loss, filter: this.css(result.values), }; }; /** * Solve wide values based on the wide values for RGB and HSL values * * @private * @returns {SPSAPayload} * @memberof Solver */ Solver.prototype.solveWide = function () { var A = 5; var c = 15; // Wide values for RGB and HSL values // the values in the order: [`r`, `g`, `b`, `h`, `s`, `l`] var a = [60, 180, 18000, 600, 1.2, 1.2]; var best = { loss: Infinity }; var counter = 0; while (best.loss > this.options.acceptanceLossPercentage) { var initialFilterValues = [50, 20, 3750, 50, 100, 100]; var result = this.spsa({ A: A, a: a, c: c, values: initialFilterValues, // for wide values we should use the double of tries in // comparison of `solveNarrow()` method maxTriesInLoop: 1000, }); if (result.loss < best.loss) { best = result; } counter += 1; if (counter >= this.options.maxChecks) { break; } } return Object.assign({}, best, { called: counter }); }; /** * Solve narrow values based on the wide values for the filter * * @private * @param {SPSAPayload} wide * @returns {SPSAPayload} * @memberof Solver */ Solver.prototype.solveNarrow = function (wide) { var A = wide.loss; var c = 2; var A1 = A + 1; // Narrow values for RGB and HSL values // the values in the order: [`r`, `g`, `b`, `h`, `s`, `l`] var a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1]; return this.spsa({ A: A, a: a, c: c, values: wide.values, maxTriesInLoop: 500, called: wide.called, }); }; /** * Returns final value based on the current filter order * to get the order, please check the returned value * in `css()` method * * @private * @param {number} value * @param {number} idx * @returns {number} * @memberof Solver */ Solver.prototype.fixValueByFilterIDX = function (value, idx) { var max = 100; // Fixing max, minimum and value by filter if (idx === 2 /* saturate */) { max = 7500; } else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; } if (idx === 3 /* hue-rotate */) { if (value > max) { value %= max; } else if (value < 0) { value = max + (value % max); } } // Checking if value is below the minimum or above // the maximum allowed by filter else if (value < 0) { value = 0; } else if (value > max) { value = max; } return value; }; Solver.prototype.spsa = function (_a) { var A = _a.A, a = _a.a, c = _a.c, values = _a.values, _b = _a.maxTriesInLoop, maxTriesInLoop = _b === void 0 ? 500 : _b, _c = _a.called, called = _c === void 0 ? 0 : _c; var alpha = 1; var gamma = 0.16666666666666666; var best = null; var bestLoss = Infinity; var deltas = new Array(6); var highArgs = new Array(6); var lowArgs = new Array(6); // Size of all CSS filters to be applied to get the correct color var filtersToBeAppliedSize = 6; for (var key = 0; key < maxTriesInLoop; key++) { var ck = c / Math.pow(key + 1, gamma); for (var i = 0; i < filtersToBeAppliedSize; i++) { deltas[i] = Math.random() > 0.5 ? 1 : -1; highArgs[i] = values[i] + ck * deltas[i]; lowArgs[i] = values[i] - ck * deltas[i]; } var lossDiff = this.loss(highArgs) - this.loss(lowArgs); for (var i = 0; i < filtersToBeAppliedSize; i++) { var g = (lossDiff / (2 * ck)) * deltas[i]; var ak = a[i] / Math.pow(A + key + 1, alpha); values[i] = this.fixValueByFilterIDX(values[i] - ak * g, i); } var loss = this.loss(values); if (loss < bestLoss) { best = values.slice(0); bestLoss = loss; } } return { values: best, loss: bestLoss, called: called }; }; /** * Checks how much is the loss for the filter in RGB and HSL colors * * @private * @param {SPSAPayload['values']} filters * @returns {number} * @memberof Solver */ Solver.prototype.loss = function (filters) { // Argument as an Array of percentages. var color = this.reusedColor; // Resetting the color to black in case // it was called more than once color.set(0, 0, 0); color.invert(filters[0] / 100); color.sepia(filters[1] / 100); color.saturate(filters[2] / 100); color.hueRotate(filters[3] * 3.6); color.brightness(filters[4] / 100); color.contrast(filters[5] / 100); var colorHSL = color.hsl(); return (Math.abs(color.r - this.target.r) + Math.abs(color.g - this.target.g) + Math.abs(color.b - this.target.b) + Math.abs(colorHSL.h - this.targetHSL.h) + Math.abs(colorHSL.s - this.targetHSL.s) + Math.abs(colorHSL.l - this.targetHSL.l)); }; /** * Returns the CSS filter list for the received HEX color * * @private * @param {number[]} filters * @returns {string} * @memberof Solver */ Solver.prototype.css = function (filters) { var formatCssFilterValueByMultiplier = function (idx, multiplier) { if (multiplier === void 0) { multiplier = 1; } return Math.round(filters[idx] * multiplier); }; return [ "invert(".concat(formatCssFilterValueByMultiplier(0), "%)"), "sepia(".concat(formatCssFilterValueByMultiplier(1), "%)"), "saturate(".concat(formatCssFilterValueByMultiplier(2), "%)"), "hue-rotate(".concat(formatCssFilterValueByMultiplier(3, 3.6), "deg)"), "brightness(".concat(formatCssFilterValueByMultiplier(4), "%)"), "contrast(".concat(formatCssFilterValueByMultiplier(5), "%)"), ].join(' '); }; return Solver; }()); /** * Transform a CSS Color from Hexadecimal to RGB color * * @param {string} hex hexadecimal color * @returns {([number, number, number] | [])} array with the RGB colors or empty array */ var hexToRgb = function (hex) { if (hex.length === 4) { return [parseInt("0x".concat(hex[1]).concat(hex[1])), parseInt("0x".concat(hex[2]).concat(hex[2])), parseInt("0x".concat(hex[3]).concat(hex[3]))]; } if (hex.length === 7) { return [parseInt("0x".concat(hex[1]).concat(hex[2])), parseInt("0x".concat(hex[3]).concat(hex[4])), parseInt("0x".concat(hex[5]).concat(hex[6]))]; } return []; }; var isNumeric = function (n) { return !isNaN(parseFloat(n)) && isFinite(n); }; // Memory cache for the computed results to avoid multiple // calculations for the same color var results = {}; /** * A function that transforms a HEX color into CSS filters * * @param colorValue string hexadecimal color * @param opts HexToCssConfiguration function configuration * */ var hexToCSSFilter = function (colorValue, opts) { var _a; if (opts === void 0) { opts = {}; } var red; var green; var blue; if (results[colorValue] && !opts.forceFilterRecalculation) { return Object.assign({}, results[colorValue], { cache: true }); } var color; try { _a = __read(hexToRgb(colorValue), 3), red = _a[0], green = _a[1], blue = _a[2]; if (!isNumeric(red) || !isNumeric(green) || !isNumeric(blue)) { throw new Error("hextToRgb returned an invalid value for '".concat(colorValue, "'")); } color = new Color(Number(red), Number(green), Number(blue)); } catch (error) { throw new Error("Color value should be in HEX format. ".concat(error)); } var solver = new Solver(color, Object.assign({}, // `HexToCssConfiguration` Defaults { acceptanceLossPercentage: 5, maxChecks: 30, forceFilterRecalculation: false, }, opts)); return (results[colorValue] = Object.assign({}, solver.solve(), { hex: colorValue, rgb: [red, green, blue], cache: false, })); }; /** * A function that clears cached results * * @param {string} key? HEX string value passed previously `#24639C`. If not passed, it clears all cached results * @returns void */ var clearCache = function (key) { if (!key) { results = {}; } else if (results[key]) { delete results[key]; } }; exports.clearCache = clearCache; exports.hexToCSSFilter = hexToCSSFilter; Object.defineProperty(exports, '__esModule', { value: true }); }));