├── .circleci
└── config.yml
├── .coveralls.yml
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
└── ISSUE_TEMPLATE.md
├── .gitignore
├── .husky
└── pre-commit
├── .lintstagedrc
├── .npmignore
├── .nvmrc
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bundlesize.config.json
├── contributing.md
├── jest.config.js
├── jest.setup.js
├── package.json
├── rollup.config.js
├── src
├── __tests__
│ └── hex-to-css-filter.ts
├── color.ts
├── hex-to-css-filter.ts
├── index.ts
└── solver.ts
├── tsconfig.eslint.json
├── tsconfig.json
├── tsconfig.spec.json
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | docker_defaults: &docker_defaults
4 | docker:
5 | - image: circleci/node:14.15.5-browsers
6 | working_directory: ~/project/repo
7 |
8 | attach_workspace: &attach_workspace
9 | attach_workspace:
10 | at: ~/project
11 |
12 | install_steps: &install_steps
13 | steps:
14 | - checkout
15 | - restore_cache:
16 | name: Restore node_modules cache
17 | keys:
18 | - dependency-cache-{{ .Branch }}-{{ checksum "package.json" }}
19 | - dependency-cache-{{ .Branch }}-
20 | - dependency-cache-
21 | - run:
22 | name: Installing Dependencies
23 | command: |
24 | yarn install --silent --frozen-lockfile
25 | - save_cache:
26 | name: Save node_modules cache
27 | key: dependency-cache-{{ .Branch }}-{{ checksum "yarn.lock" }}
28 | paths:
29 | - node_modules/
30 | - persist_to_workspace:
31 | root: ~/project
32 | paths:
33 | - repo
34 |
35 | workflows:
36 | version: 2
37 | build_pipeline:
38 | jobs:
39 | - build
40 | - unit_test:
41 | requires:
42 | - build
43 | - bundle_size:
44 | requires:
45 | - build
46 | jobs:
47 | build:
48 | <<: *docker_defaults
49 | <<: *install_steps
50 | unit_test:
51 | <<: *docker_defaults
52 | steps:
53 | - *attach_workspace
54 | - run:
55 | name: Running unit tests
56 | command: |
57 | sudo yarn test:ci
58 | bundle_size:
59 | <<: *docker_defaults
60 | steps:
61 | - *attach_workspace
62 | - run:
63 | name: Checking bundle size
64 | command: |
65 | sudo yarn build
66 | sudo yarn bundlesize
67 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | repo_token: t3pzmJK46PDJB3gcU9Q6Zk1iRZ7FYkZG5
2 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | # Change these settings to your own preference
9 | indent_style = space
10 | indent_size = 2
11 |
12 | # We recommend you to keep these unchanged
13 | end_of_line = lf
14 | charset = utf-8
15 | trim_trailing_whitespace = true
16 | insert_final_newline = true
17 |
18 | [*.md]
19 | trim_trailing_whitespace = false
20 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .github
2 | .jest
3 | coverage
4 | lib
5 | node_modules
6 | flow-typed
7 | .yarn
8 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true,
5 | node: true,
6 | jest: true,
7 | },
8 | extends: [
9 | 'plugin:prettier/recommended',
10 | 'plugin:@typescript-eslint/eslint-recommended',
11 | 'plugin:@typescript-eslint/recommended',
12 | ],
13 | globals: {
14 | Atomics: 'readonly',
15 | SharedArrayBuffer: 'readonly',
16 | },
17 | parser: '@typescript-eslint/parser',
18 | parserOptions: {
19 | ecmaVersion: 2018,
20 | sourceType: 'module',
21 | },
22 | plugins: ['@typescript-eslint', 'prettier'],
23 | rules: {
24 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Minimal example
2 |
3 | Add here an online minimal example and share the link here, if the case.
4 |
5 | ### Expected behaviour
6 |
7 | I thought that by going to the page '...' and do the action '...' then '...' would happen.
8 |
9 | ### Actual behaviour
10 |
11 | Instead of '...', what I saw was that '...' happened instead.
12 |
13 | ### How to simulate the behaviour (if applicable)
14 |
15 | This is the example of the current behaviour https://stackblitz.com/edit/hex-to-css-filter-playground
16 |
17 | ### Browsers/OS/Platforms affected
18 |
19 | Please specify the affected browsers/OS/platforms.
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib
2 | npm-debug.log
3 | .DS_Store
4 | node_modules
5 | coverage
6 | package
7 | .nyc_output
8 | dist
9 | .jest/
10 | out-tsc/
11 | *.js
12 | !rollup.config.js
13 | *.d.ts
14 | !jest.*.js
15 | !scripts/*
16 | *.tgz
17 | !.eslintrc.js
18 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn lint-staged
5 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.{js,ts}": [
3 | "prettier --no-editorconfig --write"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /.*/
2 | /demo/
3 | .*
4 | bower.json
5 | karma.conf.js
6 | src
7 | coverage
8 | .nyc_output
9 | .jest
10 | **/__tests__/
11 | out-tsc
12 | images
13 | scripts
14 | jest*.js
15 | tsconfig.*.json
16 | tsconfig.json
17 | .coveralls.yml
18 | .banner
19 | package
20 | *.tgz
21 |
22 | # ignore OS generated files
23 | **/.DS_Store
24 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v14.15.5
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "printWidth": 120,
4 | "tabWidth": 2,
5 | "useTabs": false,
6 | "singleQuote": true,
7 | "trailingComma": "all",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": false,
10 | "arrowParens": "avoid"
11 | }
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/)
6 | and this project adheres to [Semantic Versioning](http://semver.org/).
7 |
8 | ## [Unreleased][]
9 |
10 | ## [6.0.0][] - 2025-03-11
11 |
12 | ### Fixed
13 |
14 | - Removing semicolon at the end of filter values. So that, consumers will be able to use its value in any order. E.G:
15 |
16 | After changes
17 |
18 | ```css
19 | /* ✅ Both approaches will work as expected 🎉 */
20 | filter: var(--hex-to-css-filter) var(--blur-filter);
21 | filter: var(--blur-filter) var(--hex-to-css-filter);
22 | ```
23 |
24 | Before changes
25 |
26 | ```css
27 | /* ❌ This order was NOT working before changes */
28 | filter: var(--hex-to-css-filter) var(--blur-filter);
29 | /* ✅ This order was the only way of applying that */
30 | filter: var(--blur-filter) var(--hex-to-css-filter);
31 | ```
32 |
33 |
34 |
35 | ## [5.4.0][] - 2022-03-29
36 |
37 | ### Fixed
38 |
39 | - Fixing type distribution issue
40 |
41 | ## [5.3.0][] - 2022-03-03
42 |
43 | ### Fixed
44 |
45 | - Fixing build distribution issue during install
46 |
47 | ## [5.2.0][] - 2021-09-16
48 |
49 | ### Fixed
50 |
51 | - Fixing install command package calling `husky install`
52 |
53 | ## [5.1.0][] - 2021-08-08
54 |
55 | ### Fixed
56 |
57 | - Fixing issue when passing a key to be removed from cache that doesn't exist. Before the fix, if the consumer was passing a non-existent key, all the cached values were removed. Now, if a non-existent key is passed through the cache won't be changed
58 |
59 | ### Updated
60 |
61 | - Adding JSDocs for `clearCache` method
62 | - Adding `.lintstagedrc` configuration file
63 | - Applying changes to decrese bundle size to 2.2KB 🎉
64 |
65 | ## [5.0.0][] - 2021-08-07
66 |
67 | ### Updated
68 |
69 | - Updating NodeJS version to v14.15.5
70 | - Upgrading dependencies and devDependencies
71 |
72 | ### Added
73 |
74 | - Adding `clearCache` function to removed values from memory cache. It also gives the option of clear all cached values from memory.
75 |
76 | ```ts
77 | // Creating CSS filters for `#24639C` and `#FF0000`
78 | // They memory cache stored is based on the received hex value
79 | const [firstResult, secondResult, thirdResult, forthResult] = [
80 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
81 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
82 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
83 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
84 | ];
85 |
86 | // ...
87 | // ✨ Here is the place where the magic happens in your App ✨
88 | // ...
89 |
90 | // Removing the memory cache only for `#24639C`
91 | // It means that `#FF0000` is still cached.
92 | // It's quite handy in scenarios of colors that are called for several times,
93 | // Having other ones called twice or thrice
94 | clearCache('#24639C');
95 |
96 | // Or you can just remove all cached values from memory
97 | // by calling the function with no arguments
98 | clearCache();
99 |
100 | // `fifthResult` and `sixthResult` will/won't be computed again based on `clearCache` usage
101 | const [fifthResult, sixthResult] = [
102 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
103 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
104 | ];
105 | ```
106 |
107 | ## [4.0.0][] - 2021-05-09
108 |
109 | ### Updated
110 |
111 | - Updating the project dependencies and devDependencies to the latest version
112 | - Decreasing package bundle size
113 |
114 | ## [3.1.2][] - 2020-07-24
115 |
116 | ### Fixed
117 |
118 | - Fixing UMD bundle by using Rollup. Typescript was required in the package and one of the TS functions is required in the bundle.
119 |
120 | ## [3.1.1][] - 2020-06-29
121 |
122 | ### Updated
123 |
124 | - Updating package dependencies and devDependencies to the latest
125 |
126 | ## [3.1.0][] - 2020-06-29
127 |
128 | ### Added
129 |
130 | - Adding boolean `cache` field into the `hexToCSSFilter()` payload;
131 | - Adding
132 | ynew configuration option: `forceFilterRecalculation`. It's a boolean value that forces recalculation for CSS filter generation. Default: `false`;
133 |
134 | ### Fixed
135 |
136 | - Fixing color generation using `maxTriesInLoop` to get the optimal color for the CSS filter
137 |
138 | ## [3.0.1][] - 2020-06-28
139 |
140 | ### Updated
141 |
142 | - Increasing `maxChecks` from 15 to 30
143 | - Adding private methods in classes
144 | - Improving internal types
145 | - Removing `any` from codebase
146 |
147 | ## [3.0.0][] - 2020-06-25
148 |
149 | ### Fixed
150 |
151 | - CSS Filter working properly when receives `#FFF` color;
152 | - Fixed internal issue on `hexToRgb` method when receiving `#FFF` and `#000` colors
153 |
154 | ### Updated
155 |
156 | - Breaking change: `HexToCssConfiguration` type now is using `acceptanceLossPercentage` instead of `acceptableLossPercentage`
157 |
158 | ```
159 | - acceptableLossPercentage?: number;
160 | + acceptanceLossPercentage?: number;
161 | ```
162 |
163 | - Better types for internal methods
164 | - Improving package documentation
165 | - Adding documentation for consumers to use `#000` as a container background on `README.md`
166 |
167 | ## [2.0.4][] - 2020-04-24
168 |
169 | ### Fixed
170 |
171 | - `Solver`: Changing default target color to be white or black, based on the
172 | given color. It solves the issue when a color is darker and the returned CSS filter resolutions is incorrect.
173 |
174 | E.G. https://codepen.io/willmendesneto/pen/pOVGVe
175 |
176 | With the issue
177 |
178 |
179 | Without the issue
180 |
181 |
182 | ## [2.0.3][] - 2020-04-24
183 |
184 | ### Fixed
185 |
186 | - Fixing bundle size
187 | - Setting the filter to white to take effect properly. Closes https://github.com/willmendesneto/hex-to-css-filter/issues/7
188 |
189 | Since `Solver` is forcing the stored instance of `color` to be white in rgb, the brightness should be white as well. That
190 | means the filter is based on white, so it needs to set the filter to white to take effect.
191 |
192 | https://github.com/willmendesneto/hex-to-css-filter/blob/996d0c78ba275b7c16ae3d87821dd044276db563/src/solver.ts#L136
193 |
194 | E.G.
195 |
196 | ```diff
197 | - filter: invert(39%) sepia(91%) saturate(4225%) hue-rotate(162deg) brightness(95%) contrast(101%);
198 | + filter: brightness(0) invert(1) invert(39%) sepia(91%) saturate(4225%) hue-rotate(162deg) brightness(95%) contrast(101%);
199 | ```
200 |
201 | ## [2.0.2][] - 2020-04-09
202 |
203 | ### Updated
204 |
205 | - Updating description
206 | - Removing broken link
207 |
208 | ## [2.0.1][] - 2020-04-09
209 |
210 | ### Updated
211 |
212 | - Bumped dependencies
213 | - Upgraded NodeJS to 12.14.1
214 | - Updated README.md with proper docs
215 |
216 | ### Fixed
217 |
218 | - Fixed CircleCI pipeline
219 | - Fixed Uglify issue on build task
220 | - Fixed bundlesize task
221 | - Fixed ESLint issue after upgrade
222 |
223 | ## [2.0.0][] - 2020-01-09
224 |
225 | ### Updated
226 |
227 | - Migrating package to Typescript
228 |
229 | BREAKING CHANGE:
230 |
231 | To improve readability, these type definitions were renamed
232 |
233 | - `Option` was renamed to `HexToCssConfiguration`;
234 | - `ReturnValue` was renamed to `HexToCssResult`;
235 |
236 | ## [1.0.3][] - 2019-12-18
237 |
238 | ### Added
239 |
240 | - Adding typescript types for package
241 |
242 | ## [1.0.2][] - 2018-09-14
243 |
244 | ### Updated
245 |
246 | - Returning RGB as an array with red, green and blue values
247 |
248 | ## [1.0.1][] - 2018-09-13
249 |
250 | ### Added
251 |
252 | - First version of the package
253 | - Adding memory cache to store the computed result
254 |
255 | ### Updated
256 |
257 | - Minor code changes to decrese bundle size to 2.2KB 🎉
258 | - Using `Object.assign()` to return the best match object
259 | - Changing the color to check: from white to black
260 |
261 | ### Fixed
262 |
263 | - Fixing editorconfig code style
264 |
265 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v1.0.2...HEAD
266 | [1.0.2]: https://github.com/willmendesneto/hex-to-css-filter/compare/v1.0.1...v1.0.2
267 | [1.0.1]: https://github.com/willmendesneto/hex-to-css-filter/tree/v1.0.1
268 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v1.0.3...HEAD
269 | [1.0.3]: https://github.com/willmendesneto/hex-to-css-filter/tree/v1.0.3
270 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v2.0.0...HEAD
271 | [2.0.0]: https://github.com/willmendesneto/hex-to-css-filter/tree/v2.0.0
272 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v2.0.2...HEAD
273 | [2.0.2]: https://github.com/willmendesneto/hex-to-css-filter/compare/v2.0.1...v2.0.2
274 | [2.0.1]: https://github.com/willmendesneto/hex-to-css-filter/tree/v2.0.1
275 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v2.0.3...HEAD
276 | [2.0.3]: https://github.com/willmendesneto/hex-to-css-filter/tree/v2.0.3
277 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v2.0.4...HEAD
278 | [2.0.4]: https://github.com/willmendesneto/hex-to-css-filter/tree/v2.0.4
279 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v3.0.0...HEAD
280 | [3.0.0]: https://github.com/willmendesneto/hex-to-css-filter/tree/v3.0.0
281 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v3.0.1...HEAD
282 | [3.0.1]: https://github.com/willmendesneto/hex-to-css-filter/tree/v3.0.1
283 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v3.1.0...HEAD
284 | [3.1.0]: https://github.com/willmendesneto/hex-to-css-filter/tree/v3.1.0
285 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v3.1.1...HEAD
286 | [3.1.1]: https://github.com/willmendesneto/hex-to-css-filter/tree/v3.1.1
287 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v3.1.2...HEAD
288 | [3.1.2]: https://github.com/willmendesneto/hex-to-css-filter/tree/v3.1.2
289 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v4.0.0...HEAD
290 | [4.0.0]: https://github.com/willmendesneto/hex-to-css-filter/tree/v4.0.0
291 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v5.0.0...HEAD
292 | [5.0.0]: https://github.com/willmendesneto/hex-to-css-filter/tree/v5.0.0
293 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v5.1.0...HEAD
294 | [5.1.0]: https://github.com/willmendesneto/hex-to-css-filter/tree/v5.1.0
295 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v5.2.0...HEAD
296 | [5.2.0]: https://github.com/willmendesneto/hex-to-css-filter/tree/v5.2.0
297 | [unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v5.3.0...HEAD
298 | [5.3.0]: https://github.com/willmendesneto/hex-to-css-filter/tree/v5.3.0
299 |
300 |
301 | [Unreleased]: https://github.com/willmendesneto/hex-to-css-filter/compare/v6.0.0...HEAD
302 | [6.0.0]: https://github.com/willmendesneto/hex-to-css-filter/compare/v5.4.0...v6.0.0
303 | [5.4.0]: https://github.com/willmendesneto/hex-to-css-filter/tree/v5.4.0
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Will Mendes
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hex-to-css-filter
2 |
3 | [](https://greenkeeper.io/)
4 | [](https://stackblitz.com/edit/hex-to-css-filter-playground)
5 |
6 | [](http://badge.fury.io/js/hex-to-css-filter) [](https://npmjs.org/hex-to-css-filter)
7 | [](LICENSE)
8 |
9 | [](https://circleci.com/gh/willmendesneto/hex-to-css-filter)
10 | [](https://coveralls.io/r/willmendesneto/hex-to-css-filter?branch=master)
11 | [](https://david-dm.org/willmendesneto/hex-to-css-filter)
12 |
13 | [](https://npmjs.org/hex-to-css-filter)
14 | [](https://npmjs.org/hex-to-css-filter)
15 |
16 | > Easy way to generate colors from HEX to CSS Filters 😎
17 |
18 | ## Contributing
19 |
20 | Please check our [contributing.md](https://github.com/willmendesneto/hex-to-css-filter/blob/master/contributing.md) to know more about setup and how to contribute.
21 |
22 | ## Setup and installation
23 |
24 | Make sure that you are using the NodeJS version is the same as `.nvmrc` file version. If you don't have this version please use a version manager such as `nvm` or `n` to manage your local nodejs versions.
25 |
26 | > Please make sure that you are using NodeJS version 6.10.2
27 |
28 | Assuming that you are using `nvm`, please run the commands inside this folder:
29 |
30 | ```bash
31 | $ nvm install $(cat .nvmrc); # install required nodejs version
32 | $ nvm use $(cat .nvmrc); # use nodejs version
33 | ```
34 |
35 | In Windows, please install NodeJS using one of these options:
36 |
37 | Via `NVM Windows` package: Dowload via [this link](https://github.com/coreybutler/nvm-windows). After that, run the commands:
38 |
39 | ```bash
40 | $ nvm install $(cat .nvmrc); # install required nodejs version
41 | $ nvm use $(cat .nvmrc); # use nodejs version
42 | ```
43 |
44 | Via Chocolatey:
45 |
46 | ```bash
47 | $ choco install nodejs.install -version 6.10.2
48 | ```
49 |
50 | ### Install yarn
51 |
52 | We use `yarn` as our package manager instead of `npm`
53 |
54 | [Install it following these steps](https://yarnpkg.com/lang/en/docs/install/#mac-tab)
55 |
56 | After that, just navigate to your local repository and run
57 |
58 | ```bash
59 | $ yarn install && yarn husky:install
60 | ```
61 |
62 | ## Demo
63 |
64 | Try out our [demo on Stackblitz](https://hex-to-css-filter-playground.stackblitz.io)!
65 |
66 | ### Run the tests
67 |
68 | ```bash
69 | $ yarn test # run the tests
70 | ```
71 |
72 | ### Run the build
73 |
74 | ```bash
75 | $ yarn build # run the tests
76 | ```
77 |
78 | ### Run the bundlesize check
79 |
80 | ```bash
81 | $ yarn bundlesize # run the tests
82 | ```
83 |
84 | ### Run the code lint
85 |
86 | ```bash
87 | $ yarn lint # run the tests
88 | ```
89 |
90 | ## Usage
91 |
92 | ### Important!!!!
93 |
94 | _Please make sure the background of the element is `#000` for better performance and color similarity_.
95 |
96 | The reason for this is because all the calcs done by the library to generate a CSS Filter are based on the color `#000`
97 |
98 | ### Using default options
99 |
100 | ```js
101 | import { hexToCSSFilter } from 'hex-to-css-filter';
102 |
103 | const cssFilter = hexToCSSFilter('#00a4d6');
104 | console.log(cssFilter);
105 | ```
106 |
107 | ### Overriding default options
108 |
109 | You can override the default options by passing a second parameter into `hexToCSSFilter` method. You can also use `HexToCssConfiguration` for type support on it.
110 |
111 | ```ts
112 | import { hexToCSSFilter, HexToCssConfiguration } from 'hex-to-css-filter';
113 |
114 | const config: HexToCssConfiguration = {
115 | acceptanceLossPercentage: 1,
116 | maxChecks: 10,
117 | };
118 |
119 | const cssFilter = hexToCSSFilter('#00a4d6', config);
120 | console.log(cssFilter);
121 |
122 | // Calling different colors to create CSS Filters
123 | [
124 | hexToCSSFilter('#FFF'),
125 | hexToCSSFilter('#000'),
126 | hexToCSSFilter('#802e1c'),
127 | hexToCSSFilter('#00a4d6'),
128 | hexToCSSFilter('#FF0000'),
129 | hexToCSSFilter('#173F5E'),
130 | hexToCSSFilter('#24639C'),
131 | hexToCSSFilter('#3CAEA4'),
132 | hexToCSSFilter('#F6D55C'),
133 | hexToCSSFilter('#ED553C'),
134 | ].forEach(cssFilter => {
135 | console.log(`\n${cssFilter.hex}-[${cssFilter.rgb}]: ${cssFilter.filter}`);
136 | });
137 | ```
138 |
139 | It returns an object with the values:
140 |
141 | - `cache`: returns a boolean to confirm if value was previously computed and is coming from local memory cache or not;
142 | - `called`: how many times the script was called to solve the color;
143 | - `filter`: CSS filter generated based on the HEX color;
144 | - `hex`: the received color;
145 | - `loss`: percentage loss value for the generated filter;
146 | - `rgb`: HEX color in RGB;
147 | - `values`: percentage loss per each color type organized in RGB: `red`, `green`, `blue`, `h`, `s`, `l`. Used for debug purposes - if needed;
148 |
149 | ### Options
150 |
151 | - `acceptanceLossPercentage`: Acceptable color percentage to be lost during wide search. Does not guarantee `loss`. Default: `5`;
152 | - `maxChecks`: Maximum checks that needs to be done to return the best value. Default: `10`;
153 | - `forceFilterRecalculation`: Boolean value that forces recalculation for CSS filter generation. Default: `false`;
154 |
155 | ### Removing memory cache
156 |
157 | In some cases the memory cache is quite handy. However, it doesn't need to stored after called in some cases. If you're using it in some frontend libraries/frameworks, have that in memory can become an issue.
158 |
159 | In order to solve that, you can now use the function `clearCache` to remove the memory cache. The method can receive the stored hex color. In this case, _only the received key will be removed_. E.G.
160 |
161 | ```ts
162 | // Creating CSS filters for `#24639C` and `#FF0000`
163 | // They memory cache stored is based on the received hex value
164 | const [firstResult, secondResult, thirdResult, forthResult] = [
165 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
166 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
167 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
168 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
169 | ];
170 |
171 | // ...
172 | // ✨ Here is the place where the magic happens in your App ✨
173 | // ...
174 |
175 | // Removing the memory cache only for `#24639C`
176 | // It means that `#FF0000` is still cached.
177 | // It's quite handy in scenarios of colors that are called for several times,
178 | // Having other ones called twice or thrice
179 | clearCache('#24639C');
180 |
181 | // `fifthResult` will be computed again, since there's no cache
182 | // `sixthResult` won't be computed because of the existent memory cache for the value
183 | const [fifthResult, sixthResult] = [
184 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
185 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
186 | ];
187 | ```
188 |
189 | Also, it covers the scenario of removing all the cache by calling the function with no arguments. E.G.
190 |
191 | ```ts
192 | // Creating CSS filters for `#24639C` and `#FF0000`
193 | // They memory cache stored is based on the received hex value
194 | const [firstResult, secondResult, thirdResult, forthResult] = [
195 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
196 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
197 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
198 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
199 | ];
200 |
201 | // ...
202 | // ✨ Here is the place where the magic happens in your App ✨
203 | // ...
204 |
205 | // Removing all cached values from memory
206 | clearCache();
207 |
208 | // `fifthResult` and `sixthResult` will be computed again, since there's no cache
209 | const [fifthResult, sixthResult] = [
210 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
211 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
212 | ];
213 | ```
214 |
215 | ## Publish
216 |
217 | this project is using `np` package to publish, which makes things straightforward. EX: `np `
218 |
219 | > For more details, [please check np package on npmjs.com](https://www.npmjs.com/package/np)
220 |
221 | ## Author
222 |
223 | **Wilson Mendes (willmendesneto)**
224 |
225 | -
226 | -
227 |
--------------------------------------------------------------------------------
/bundlesize.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | {
4 | "path": "./dist/esm/index.js",
5 | "maxSize": "57B"
6 | },
7 | {
8 | "path": "./dist/es2015/index.js",
9 | "maxSize": "57B"
10 | },
11 | {
12 | "path": "./dist/cjs/index.js",
13 | "maxSize": "154B"
14 | },
15 | {
16 | "path": "./dist/umd/hex-to-css-filter.js",
17 | "maxSize": "5.2KB"
18 | },
19 | {
20 | "path": "./dist/umd/hex-to-css-filter.min.js",
21 | "maxSize": "2.3KB"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing guide
2 |
3 | Want to contribute to hex-to-css-filter package? Awesome!
4 | There are many ways you can contribute, see below.
5 |
6 | ## Opening issues
7 |
8 | Open an issue to report bugs or to propose new features.
9 |
10 | - Reporting bugs: describe the bug as clearly as you can, including steps to reproduce, what happened and what you were expecting to happen. Also include browser version, OS and other related software's (npm, Node.js, etc) versions when applicable.
11 |
12 | - Proposing features: explain the proposed feature, what it should do, why it is useful, how users should use it. Give us as much info as possible so it will be easier to discuss, access and implement the proposed feature. When you're unsure about a certain aspect of the feature, feel free to leave it open for others to discuss and find an appropriate solution. In case of a specific scenario, please recreate that in our Stackblitz demo (http://stackblitz.com/edit/hex-to-css-filter-playground) and share the link. This will help in make the fix as quick as possible
13 |
14 | ## Proposing pull requests
15 |
16 | Pull requests are very welcome. Note that if you are going to propose drastic changes, be sure to open an issue for discussion first, to make sure that your PR will be accepted before you spend effort coding it.
17 |
18 | Fork the hex-to-css-filter repository, clone it locally and create a branch for your proposed bug fix or new feature. Avoid working directly on the master branch.
19 |
20 | Implement your bug fix or feature, write tests to cover it and make sure all tests are passing (run a final `npm test` to make sure everything is correct). Then commit your changes, push your bug fix/feature branch to the origin (your forked repo) and open a pull request to the upstream (the repository you originally forked)'s master branch.
21 |
22 | ## Documentation
23 |
24 | Documentation is extremely important and takes a fair deal of time and effort to write and keep updated. Please submit any and all improvements you can make to the repository's docs.
25 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | globals: {
3 | 'ts-jest': {
4 | tsConfig: './tsconfig.spec.json',
5 | },
6 | },
7 | automock: false,
8 | cacheDirectory: '/.jest',
9 | collectCoverage: true,
10 | roots: ['src'],
11 | collectCoverageFrom: ['**/src/*.ts', '!src/index.ts'],
12 | coverageThreshold: {
13 | global: {
14 | branches: 80,
15 | functions: 92,
16 | lines: 96,
17 | statements: 94,
18 | },
19 | },
20 | preset: 'ts-jest',
21 | testEnvironment: 'node',
22 | testPathIgnorePatterns: ['/node_modules/', '/scripts/', '/dist/'],
23 | setupFilesAfterEnv: ['/jest.setup.js'],
24 | };
25 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-undef
2 | const { JSDOM } = require('jsdom');
3 |
4 | const jsdom = new JSDOM('');
5 | const { window } = jsdom;
6 |
7 | global.window = window;
8 | global.document = window.document;
9 | global.navigator = {
10 | userAgent: 'node.js',
11 | };
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hex-to-css-filter",
3 | "version": "6.0.0",
4 | "description": "hex-to-css-filter - Easy way to generate colors from HEX to CSS Filters",
5 | "author": "Will Mendes ",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/willmendesneto/hex-to-css-filter.git"
9 | },
10 | "bugs": {
11 | "url": "https://github.com/willmendesneto/hex-to-css-filter/issues"
12 | },
13 | "homepage": "https://github.com/willmendesneto/hex-to-css-filter#readme",
14 | "sideEffects": false,
15 | "license": "MIT",
16 | "browser": "dist/umd/hex-to-css-filter.js",
17 | "jsnext:main": "dist/esm/index.js",
18 | "module": "dist/esm/index.js",
19 | "main": "dist/cjs/index.js",
20 | "es2015": "dist/cjs/index.js",
21 | "cjs": "dist/cjs/index.js",
22 | "types": "dist/umd/index.d.ts",
23 | "keywords": [
24 | "hex-to-css-filter",
25 | "css",
26 | "filter",
27 | "hex",
28 | "color"
29 | ],
30 | "dependencies": {
31 | "tslib": "^2.3.0"
32 | },
33 | "devDependencies": {
34 | "@types/jest": "^26.0.24",
35 | "@types/node": "^14.0.27",
36 | "@typescript-eslint/eslint-plugin": "^4.29.0",
37 | "@typescript-eslint/parser": "^4.29.0",
38 | "bundlesize": "^0.18.0",
39 | "changelog-verify": "^1.1.0",
40 | "coveralls": "^3.1.1",
41 | "eslint": "^7.32.0",
42 | "eslint-config-prettier": "^8.3.0",
43 | "eslint-plugin-compat": "^3.11.1",
44 | "eslint-plugin-prettier": "^3.1.1",
45 | "husky": "^7.0.1",
46 | "jest": "^27.0.6",
47 | "jsdom": "^16.7.0",
48 | "lint-staged": "^11.1.2",
49 | "prettier": "^2.3.2",
50 | "rollup": "^2.56.0",
51 | "rollup-plugin-dts": "^4.2.0",
52 | "rollup-plugin-node-resolve": "^5.2.0",
53 | "ts-jest": "^27.0.4",
54 | "ts-node": "^10.1.0",
55 | "typescript": "^4.3.5",
56 | "typings": "^2.1.1",
57 | "uglify-js": "^3.14.1",
58 | "version-changelog": "^3.1.0"
59 | },
60 | "engines": {
61 | "node": ">=6.10.2"
62 | },
63 | "scripts": {
64 | "prepare": "husky install",
65 | "compile": "tsc",
66 | "clean": "rm -rf ./dist ./.jest ./coverage ./lib",
67 | "build": "yarn build:es2015 && yarn build:cjs && yarn build:esm && yarn build:umd",
68 | "build:umd": "rollup --config && yarn build:umd:min",
69 | "build:umd:min": "uglifyjs --compress --mangle --comments -o dist/umd/hex-to-css-filter.min.js -- dist/umd/hex-to-css-filter.js && gzip dist/umd/hex-to-css-filter.min.js -c > dist/umd/hex-to-css-filter.min.js.gz",
70 | "build:es2015": "tsc --module es2015 --target es2015 --outDir dist/es2015",
71 | "build:esm": "tsc --module esnext --target es5 --outDir dist/esm",
72 | "build:cjs": "tsc --module commonjs --target es5 --outDir dist/cjs",
73 | "test": "jest",
74 | "pretest:ci": "yarn lint",
75 | "test:ci": "jest --coverage --coverageReporters=text-lcov | coveralls",
76 | "check-coverage": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf coverage",
77 | "bundlesize": "bundlesize --config bundlesize.config.json",
78 | "lint": "eslint '{scripts,src}/**/*.[tj]s'",
79 | "lint:fix": "prettier --no-editorconfig --write",
80 | "version": "version-changelog CHANGELOG.md && changelog-verify CHANGELOG.md && git add CHANGELOG.md"
81 | },
82 | "browserslist": [
83 | "last 1 chrome versions",
84 | "last 1 edge versions",
85 | "last 1 firefox versions",
86 | "last 1 safari versions",
87 | "last 1 and_chr versions",
88 | "last 1 ios_saf versions"
89 | ]
90 | }
91 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve';
2 | import dts from 'rollup-plugin-dts';
3 |
4 | import { browser, module } from './package.json';
5 |
6 | const config = [
7 | {
8 | input: module,
9 | output: {
10 | file: browser,
11 | format: 'umd',
12 | name: 'HexToCSSFilter',
13 | },
14 | plugins: [resolve()],
15 | },
16 | {
17 | input: './dist/esm/index.d.ts',
18 | output: [{ file: 'dist/umd/index.d.ts', format: 'es' }],
19 | plugins: [dts()],
20 | },
21 | ];
22 |
23 | export default config;
24 |
--------------------------------------------------------------------------------
/src/__tests__/hex-to-css-filter.ts:
--------------------------------------------------------------------------------
1 | import { clearCache } from '../hex-to-css-filter';
2 | import { hexToCSSFilter, HexToCssConfiguration } from '../index';
3 |
4 | describe('hexToCSSFilter', () => {
5 | beforeEach(() => clearCache());
6 |
7 | it('loss should NOT be more than the default acceptance loss percentage', () => {
8 | expect(hexToCSSFilter('#00a4d6').loss <= 10).toBe(true);
9 | });
10 |
11 | it('should clear all memory cache if `clearCache` is called with no arguments', () => {
12 | const [firstResult, secondResult, thirdResult, forthResult] = [
13 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
14 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
15 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
16 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
17 | ].map(({ cache: _cache, ...rest }) => rest);
18 |
19 | expect(firstResult).toEqual(secondResult);
20 | expect(thirdResult).toEqual(forthResult);
21 |
22 | clearCache();
23 |
24 | const [fifthResult, sixthResult] = [
25 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
26 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
27 | ].map(({ cache: _cache, ...rest }) => rest);
28 |
29 | expect(fifthResult).not.toEqual(firstResult);
30 | expect(fifthResult).not.toEqual(secondResult);
31 | expect(sixthResult).not.toEqual(thirdResult);
32 | expect(sixthResult).not.toEqual(forthResult);
33 | });
34 |
35 | it('should keep memory cache as it is if `clearCache` receives value that does NOT exist in the cache', () => {
36 | const [firstResult, secondResult] = [
37 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
38 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
39 | ].map(({ cache: _cache, ...rest }) => rest);
40 |
41 | expect(firstResult).toEqual(secondResult);
42 |
43 | clearCache('#FF0000');
44 |
45 | const [thirdResult] = [hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration)].map(
46 | ({ cache: _cache, ...rest }) => rest,
47 | );
48 |
49 | expect(firstResult).toEqual(secondResult);
50 | expect(firstResult).toEqual(thirdResult);
51 | });
52 |
53 | it('should clear memory cache only for received argument if `clearCache` is called with arguments', () => {
54 | const [firstResult, secondResult, thirdResult, forthResult] = [
55 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
56 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
57 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
58 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
59 | ].map(({ cache: _cache, ...rest }) => rest);
60 |
61 | expect(firstResult).toEqual(secondResult);
62 | expect(thirdResult).toEqual(forthResult);
63 |
64 | clearCache('#24639C');
65 |
66 | const [fifthResult, sixthResult] = [
67 | hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration),
68 | hexToCSSFilter('#FF0000', { forceFilterRecalculation: false } as HexToCssConfiguration),
69 | ].map(({ cache: _cache, ...rest }) => rest);
70 |
71 | expect(fifthResult).not.toEqual(firstResult);
72 | expect(fifthResult).not.toEqual(secondResult);
73 | expect(sixthResult).toEqual(thirdResult);
74 | expect(sixthResult).toEqual(forthResult);
75 | });
76 |
77 | it('should use cache if `forceFilterRecalculation` is falsy via method configuration or is not configured', () => {
78 | expect(hexToCSSFilter('#24639C').cache).toBe(false);
79 | expect(hexToCSSFilter('#24639C', { forceFilterRecalculation: false } as HexToCssConfiguration).cache).toBe(true);
80 | expect(hexToCSSFilter('#24639C').cache).toBe(true);
81 | });
82 |
83 | it('should NOT use cache if `forceFilterRecalculation` is passed as `true` via method configuration', () => {
84 | expect(hexToCSSFilter('#F6D55C', { forceFilterRecalculation: true } as HexToCssConfiguration).cache).toBe(false);
85 | expect(hexToCSSFilter('#F6D55C', { forceFilterRecalculation: true } as HexToCssConfiguration).cache).toBe(false);
86 | expect(hexToCSSFilter('#F6D55C', { forceFilterRecalculation: true } as HexToCssConfiguration).cache).toBe(false);
87 | });
88 |
89 | it('loss should NOT check more than the default maximum value to check', () => {
90 | expect(hexToCSSFilter('#00a4d6').called <= 10).toBe(true);
91 | });
92 |
93 | it('should return RGB colors as list of values', () => {
94 | expect(hexToCSSFilter('#FF0000').rgb).toEqual([255, 0, 0]);
95 | });
96 |
97 | it('should work if receives a short HEX color', () => {
98 | expect(hexToCSSFilter('#000').hex).toBe('#000');
99 | });
100 |
101 | it('should return the same value as received in `hex` attribute', () => {
102 | expect(hexToCSSFilter('#FF0000').hex).toBe('#FF0000');
103 | });
104 |
105 | it('should throw an error if it receives an invalid color', () => {
106 | expect(() => hexToCSSFilter('invalid')).toThrowError(/Color value should be in HEX format/);
107 | // invalid color with more than 7 characters (one of the rules to get full HEX colors #000000)
108 | expect(() => hexToCSSFilter('invalid-value')).toThrowError(/Color value should be in HEX format/);
109 | });
110 |
111 | it('should return an object with the given values', () => {
112 | expect(Object.keys(hexToCSSFilter('#00a4d6')).sort()).toEqual([
113 | 'cache',
114 | 'called',
115 | 'filter',
116 | 'hex',
117 | 'loss',
118 | 'rgb',
119 | 'values',
120 | ]);
121 | });
122 |
123 | it('should return an object with the given CSS Filter values', () => {
124 | const { filter } = hexToCSSFilter('#00a4d6');
125 | expect(filter.split(' ').length).toEqual(6);
126 | expect(filter.includes('invert')).toBe(true);
127 | expect(filter.includes('sepia')).toBe(true);
128 | expect(filter.includes('saturate')).toBe(true);
129 | expect(filter.includes('hue-rotate')).toBe(true);
130 | expect(filter.includes('brightness')).toBe(true);
131 | expect(filter.includes('contrast')).toBe(true);
132 | expect(filter.includes(';')).toBe(false);
133 | });
134 |
135 | describe('When it receives options', () => {
136 | it('loss should NOT be more than the given acceptance loss percentage OR should be more AND was called at allowed maxChecks', () => {
137 | const res = hexToCSSFilter('#ED553C', { acceptanceLossPercentage: 1, maxChecks: 5 } as HexToCssConfiguration);
138 | expect(res.loss <= 1 || (res.loss > 1 && res.called === 5)).toBe(true);
139 | });
140 |
141 | it('loss should NOT check more than the given maximum value to check', () => {
142 | expect(
143 | hexToCSSFilter('#F6C6CE', { acceptanceLossPercentage: 0.01, maxChecks: 1 } as HexToCssConfiguration).called < 2,
144 | ).toBe(true);
145 | });
146 | });
147 | });
148 |
--------------------------------------------------------------------------------
/src/color.ts:
--------------------------------------------------------------------------------
1 | interface HSLData {
2 | h: number;
3 | s: number;
4 | l: number;
5 | }
6 |
7 | class Color {
8 | r = 0;
9 | g = 0;
10 | b = 0;
11 |
12 | constructor(r: number, g: number, b: number) {
13 | this.set(r, g, b);
14 | }
15 |
16 | set(r: number, g: number, b: number): void {
17 | this.r = this.clamp(r);
18 | this.g = this.clamp(g);
19 | this.b = this.clamp(b);
20 | }
21 |
22 | /**
23 | * Applying cals to get CSS filter for hue-rotate
24 | *
25 | * @param {number} [angle=0]
26 | * @memberof Color
27 | */
28 | hueRotate(angle = 0): void {
29 | angle = (angle / 180) * Math.PI;
30 | const sin = Math.sin(angle);
31 | const cos = Math.cos(angle);
32 |
33 | this.multiply([
34 | 0.213 + cos * 0.787 - sin * 0.213,
35 | 0.715 - cos * 0.715 - sin * 0.715,
36 | 0.072 - cos * 0.072 + sin * 0.928,
37 | 0.213 - cos * 0.213 + sin * 0.143,
38 | 0.715 + cos * 0.285 + sin * 0.14,
39 | 0.072 - cos * 0.072 - sin * 0.283,
40 | 0.213 - cos * 0.213 - sin * 0.787,
41 | 0.715 - cos * 0.715 + sin * 0.715,
42 | 0.072 + cos * 0.928 + sin * 0.072,
43 | ]);
44 | }
45 |
46 | /**
47 | * Applying cals to get CSS filter for grayscale
48 | *
49 | * @param {number} [value=1]
50 | * @memberof Color
51 | */
52 | grayscale(value = 1): void {
53 | this.multiply([
54 | 0.2126 + 0.7874 * (1 - value),
55 | 0.7152 - 0.7152 * (1 - value),
56 | 0.0722 - 0.0722 * (1 - value),
57 | 0.2126 - 0.2126 * (1 - value),
58 | 0.7152 + 0.2848 * (1 - value),
59 | 0.0722 - 0.0722 * (1 - value),
60 | 0.2126 - 0.2126 * (1 - value),
61 | 0.7152 - 0.7152 * (1 - value),
62 | 0.0722 + 0.9278 * (1 - value),
63 | ]);
64 | }
65 |
66 | /**
67 | * Applying cals to get CSS filter for sepia
68 | *
69 | * @param {number} [value=1]
70 | * @memberof Color
71 | */
72 | sepia(value = 1): void {
73 | this.multiply([
74 | 0.393 + 0.607 * (1 - value),
75 | 0.769 - 0.769 * (1 - value),
76 | 0.189 - 0.189 * (1 - value),
77 | 0.349 - 0.349 * (1 - value),
78 | 0.686 + 0.314 * (1 - value),
79 | 0.168 - 0.168 * (1 - value),
80 | 0.272 - 0.272 * (1 - value),
81 | 0.534 - 0.534 * (1 - value),
82 | 0.131 + 0.869 * (1 - value),
83 | ]);
84 | }
85 |
86 | /**
87 | * Applying cals to get CSS filter for saturate
88 | *
89 | * @param {number} [value=1]
90 | * @memberof Color
91 | */
92 | saturate(value = 1): void {
93 | this.multiply([
94 | 0.213 + 0.787 * value,
95 | 0.715 - 0.715 * value,
96 | 0.072 - 0.072 * value,
97 | 0.213 - 0.213 * value,
98 | 0.715 + 0.285 * value,
99 | 0.072 - 0.072 * value,
100 | 0.213 - 0.213 * value,
101 | 0.715 - 0.715 * value,
102 | 0.072 + 0.928 * value,
103 | ]);
104 | }
105 |
106 | private multiply(matrix: number[]): void {
107 | // These values are needed. It's correct because the returned values will change
108 | const newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
109 | const newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
110 | const newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
111 | this.r = newR;
112 | this.g = newG;
113 | this.b = newB;
114 | }
115 |
116 | /**
117 | * Applying cals to get CSS filter for brightness
118 | *
119 | * @param {number} [value=1]
120 | * @memberof Color
121 | */
122 | brightness(value = 1): void {
123 | this.linear(value);
124 | }
125 |
126 | /**
127 | * Applying cals to get CSS filter for contrast
128 | *
129 | * @param {number} [value=1]
130 | * @memberof Color
131 | */
132 | contrast(value = 1): void {
133 | this.linear(value, -(0.5 * value) + 0.5);
134 | }
135 |
136 | private linear(slope = 1, intercept = 0) {
137 | this.r = this.clamp(this.r * slope + intercept * 255);
138 | this.g = this.clamp(this.g * slope + intercept * 255);
139 | this.b = this.clamp(this.b * slope + intercept * 255);
140 | }
141 |
142 | /**
143 | * Applying cals to get CSS filter for invert
144 | *
145 | * @param {number} [value=1]
146 | * @memberof Color
147 | */
148 | invert(value = 1): void {
149 | this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
150 | this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
151 | this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
152 | }
153 |
154 | /**
155 | * transform RGB into HSL values
156 | *
157 | * @returns {HSLData}
158 | * @memberof Color
159 | */
160 | hsl(): HSLData {
161 | const red = this.r / 255;
162 | const green = this.g / 255;
163 | const blue = this.b / 255;
164 |
165 | // find greatest and smallest channel values
166 | const max = Math.max(red, green, blue);
167 | const min = Math.min(red, green, blue);
168 |
169 | let hue = 0;
170 | let saturation = 0;
171 | const lightness = (max + min) / 2;
172 |
173 | // If min and max have the same values, it means
174 | // the given color is achromatic
175 | if (max === min) {
176 | return {
177 | h: 0,
178 | s: 0,
179 | l: lightness * 100,
180 | };
181 | }
182 |
183 | // Adding delta value of greatest and smallest channel values
184 | const delta = max - min;
185 |
186 | saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);
187 |
188 | if (max === red) {
189 | hue = (green - blue) / delta + (green < blue ? 6 : 0);
190 | } else if (max === green) {
191 | hue = (blue - red) / delta + 2;
192 | } else if (max === blue) {
193 | hue = (red - green) / delta + 4;
194 | }
195 |
196 | hue /= 6;
197 |
198 | return {
199 | h: hue * 100,
200 | s: saturation * 100,
201 | l: lightness * 100,
202 | };
203 | }
204 |
205 | /**
206 | * Normalize the value to follow the min and max for RGB colors
207 | * min: 0
208 | * max: 255
209 | *
210 | * @private
211 | * @param {number} value
212 | * @returns {number}
213 | * @memberof Color
214 | */
215 | private clamp(value: number): number {
216 | // Minimum RGB Value = 0;
217 | // Maximum RGB Value = 255;
218 | return Math.min(Math.max(value, 0), 255);
219 | }
220 | }
221 |
222 | export { Color };
223 |
--------------------------------------------------------------------------------
/src/hex-to-css-filter.ts:
--------------------------------------------------------------------------------
1 | import { Solver } from './solver';
2 | import { Color } from './color';
3 |
4 | /**
5 | * Transform a CSS Color from Hexadecimal to RGB color
6 | *
7 | * @param {string} hex hexadecimal color
8 | * @returns {([number, number, number] | [])} array with the RGB colors or empty array
9 | */
10 | const hexToRgb = (hex: string): [number, number, number] | [] => {
11 | if (hex.length === 4) {
12 | return [parseInt(`0x${hex[1]}${hex[1]}`), parseInt(`0x${hex[2]}${hex[2]}`), parseInt(`0x${hex[3]}${hex[3]}`)] as [
13 | number,
14 | number,
15 | number,
16 | ];
17 | }
18 |
19 | if (hex.length === 7) {
20 | return [parseInt(`0x${hex[1]}${hex[2]}`), parseInt(`0x${hex[3]}${hex[4]}`), parseInt(`0x${hex[5]}${hex[6]}`)] as [
21 | number,
22 | number,
23 | number,
24 | ];
25 | }
26 |
27 | return [];
28 | };
29 |
30 | const isNumeric = (n: unknown): boolean => !isNaN(parseFloat(n as string)) && isFinite(n as number);
31 |
32 | // Memory cache for the computed results to avoid multiple
33 | // calculations for the same color
34 | let results: {
35 | [k: string]: HexToCssResult;
36 | } = {} as const;
37 |
38 | export interface HexToCssResult {
39 | /** How many times the script was called to solve the color */
40 | called: number;
41 | /** CSS filter generated based on the Hex color */
42 | filter: string;
43 | /** The received color */
44 | hex: string;
45 | /** Percentage loss value for the generated filter */
46 | loss: number;
47 | /** Hex color in RGB */
48 | rgb: [number, number, number];
49 | /** Percentage loss per each color type organized in RGB: red, green, blue, h, s, l. */
50 | values: [number, number, number, number, number, number];
51 | /** Boolean that returns true if value was previously computed.
52 | * So that means the returned value is coming from the in-memory cached */
53 | cache: boolean;
54 | }
55 |
56 | export interface HexToCssConfiguration {
57 | /**
58 | * Acceptable color percentage to be lost.
59 | * @default 5
60 | */
61 | acceptanceLossPercentage?: number;
62 | /**
63 | * Maximum checks that needs to be done to return the best value.
64 | * @default 10
65 | */
66 | maxChecks?: number;
67 | /**
68 | * Boolean value that forces recalculation for CSS filter generation.
69 | * @default false
70 | */
71 | forceFilterRecalculation?: boolean;
72 | }
73 |
74 | /**
75 | * A function that transforms a HEX color into CSS filters
76 | *
77 | * @param colorValue string hexadecimal color
78 | * @param opts HexToCssConfiguration function configuration
79 | *
80 | */
81 | export const hexToCSSFilter = (colorValue: string, opts: HexToCssConfiguration = {}): HexToCssResult => {
82 | let red;
83 | let green;
84 | let blue;
85 |
86 | if (results[colorValue] && !opts.forceFilterRecalculation) {
87 | return Object.assign({}, results[colorValue], { cache: true }) as HexToCssResult;
88 | }
89 |
90 | let color: Color;
91 | try {
92 | [red, green, blue] = hexToRgb(colorValue);
93 | if (!isNumeric(red) || !isNumeric(green) || !isNumeric(blue)) {
94 | throw new Error(`hextToRgb returned an invalid value for '${colorValue}'`);
95 | }
96 |
97 | color = new Color(Number(red), Number(green), Number(blue));
98 | } catch (error) {
99 | throw new Error(`Color value should be in HEX format. ${error}`);
100 | }
101 |
102 | const solver = new Solver(
103 | color,
104 | Object.assign(
105 | {},
106 | // `HexToCssConfiguration` Defaults
107 | {
108 | acceptanceLossPercentage: 5,
109 | maxChecks: 30,
110 | forceFilterRecalculation: false,
111 | },
112 | opts,
113 | ) as HexToCssConfiguration,
114 | );
115 |
116 | return (results[colorValue] = Object.assign({}, solver.solve(), {
117 | hex: colorValue,
118 | rgb: [red, green, blue],
119 | cache: false,
120 | }) as HexToCssResult);
121 | };
122 |
123 | /**
124 | * A function that clears cached results
125 | *
126 | * @param {string} key? HEX string value passed previously `#24639C`. If not passed, it clears all cached results
127 | * @returns void
128 | */
129 | export const clearCache = (key?: string): void => {
130 | if (!key) {
131 | results = {};
132 | } else if (results[key]) {
133 | delete results[key];
134 | }
135 | };
136 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hex-to-css-filter';
2 |
--------------------------------------------------------------------------------
/src/solver.ts:
--------------------------------------------------------------------------------
1 | import { Color } from './color';
2 | import { HexToCssConfiguration } from './hex-to-css-filter';
3 |
4 | interface SPSAPayload {
5 | /** How many times the script was called to solve the color */
6 | called?: number;
7 | /** Percentage loss value for the generated filter */
8 | loss: number;
9 | /** Percentage loss per each color type organized in RGB: red, green, blue, h, s, l. */
10 | values: [number, number, number, number, number, number];
11 | }
12 |
13 | class Solver {
14 | private target: Color;
15 | private targetHSL: { h: number; s: number; l: number };
16 | private reusedColor: Color;
17 | private options: { acceptanceLossPercentage: number; maxChecks: number } & HexToCssConfiguration;
18 |
19 | constructor(target: Color, options: HexToCssConfiguration) {
20 | this.target = target;
21 | this.targetHSL = target.hsl();
22 |
23 | this.options = Object.assign(
24 | {},
25 | // Adding default values for options
26 | {
27 | acceptanceLossPercentage: 5,
28 | maxChecks: 15,
29 | },
30 | options,
31 | );
32 |
33 | // All the calcs done by the library to generate
34 | // a CSS Filter are based on the color `#000`
35 | // in this case, `rgb(0, 0, 0)`
36 | // Please make sure the background of the element
37 | // is `#000` for better performance
38 | // and color similarity.
39 | this.reusedColor = new Color(0, 0, 0);
40 | }
41 |
42 | /**
43 | * Returns the solved values for the
44 | *
45 | * @returns {(SPSAPayload & { filter: string; })}
46 | * @memberof Solver
47 | */
48 | solve(): SPSAPayload & {
49 | /** CSS filter generated based on the Hex color */
50 | filter: string;
51 | } {
52 | const result = this.solveNarrow(this.solveWide());
53 | return {
54 | values: result.values,
55 | called: result.called,
56 | loss: result.loss,
57 | filter: this.css(result.values),
58 | };
59 | }
60 |
61 | /**
62 | * Solve wide values based on the wide values for RGB and HSL values
63 | *
64 | * @private
65 | * @returns {SPSAPayload}
66 | * @memberof Solver
67 | */
68 | private solveWide(): SPSAPayload {
69 | const A = 5;
70 | const c = 15;
71 | // Wide values for RGB and HSL values
72 | // the values in the order: [`r`, `g`, `b`, `h`, `s`, `l`]
73 | const a = [60, 180, 18000, 600, 1.2, 1.2];
74 |
75 | let best = { loss: Infinity };
76 | let counter = 0;
77 | while (best.loss > this.options.acceptanceLossPercentage) {
78 | const initialFilterValues: SPSAPayload['values'] = [50, 20, 3750, 50, 100, 100];
79 | const result: SPSAPayload = this.spsa({
80 | A,
81 | a,
82 | c,
83 | values: initialFilterValues,
84 | // for wide values we should use the double of tries in
85 | // comparison of `solveNarrow()` method
86 | maxTriesInLoop: 1000,
87 | });
88 |
89 | if (result.loss < best.loss) {
90 | best = result;
91 | }
92 |
93 | counter += 1;
94 | if (counter >= this.options.maxChecks) {
95 | break;
96 | }
97 | }
98 |
99 | return Object.assign({}, best, { called: counter }) as SPSAPayload;
100 | }
101 |
102 | /**
103 | * Solve narrow values based on the wide values for the filter
104 | *
105 | * @private
106 | * @param {SPSAPayload} wide
107 | * @returns {SPSAPayload}
108 | * @memberof Solver
109 | */
110 | private solveNarrow(wide: SPSAPayload): SPSAPayload {
111 | const A = wide.loss;
112 | const c = 2;
113 | const A1 = A + 1;
114 | // Narrow values for RGB and HSL values
115 | // the values in the order: [`r`, `g`, `b`, `h`, `s`, `l`]
116 | const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
117 | return this.spsa({
118 | A,
119 | a,
120 | c,
121 | values: wide.values,
122 | maxTriesInLoop: 500,
123 | called: wide.called,
124 | });
125 | }
126 |
127 | /**
128 | * Returns final value based on the current filter order
129 | * to get the order, please check the returned value
130 | * in `css()` method
131 | *
132 | * @private
133 | * @param {number} value
134 | * @param {number} idx
135 | * @returns {number}
136 | * @memberof Solver
137 | */
138 | private fixValueByFilterIDX(value: number, idx: number): number {
139 | let max = 100;
140 |
141 | // Fixing max, minimum and value by filter
142 | if (idx === 2 /* saturate */) {
143 | max = 7500;
144 | } else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) {
145 | max = 200;
146 | }
147 |
148 | if (idx === 3 /* hue-rotate */) {
149 | if (value > max) {
150 | value %= max;
151 | } else if (value < 0) {
152 | value = max + (value % max);
153 | }
154 | }
155 | // Checking if value is below the minimum or above
156 | // the maximum allowed by filter
157 | else if (value < 0) {
158 | value = 0;
159 | } else if (value > max) {
160 | value = max;
161 | }
162 | return value;
163 | }
164 |
165 | private spsa({
166 | A,
167 | a,
168 | c,
169 | values,
170 | maxTriesInLoop = 500,
171 | called = 0,
172 | }: {
173 | A: number;
174 | a: number[];
175 | c: number;
176 | values: SPSAPayload['values'];
177 | maxTriesInLoop: number;
178 | called?: number;
179 | }): SPSAPayload {
180 | const alpha = 1;
181 | const gamma = 0.16666666666666666;
182 |
183 | let best = null;
184 | let bestLoss = Infinity;
185 |
186 | const deltas = new Array(6) as SPSAPayload['values'];
187 | const highArgs = new Array(6) as SPSAPayload['values'];
188 | const lowArgs = new Array(6) as SPSAPayload['values'];
189 |
190 | // Size of all CSS filters to be applied to get the correct color
191 | const filtersToBeAppliedSize = 6;
192 |
193 | for (let key = 0; key < maxTriesInLoop; key++) {
194 | const ck = c / Math.pow(key + 1, gamma);
195 | for (let i = 0; i < filtersToBeAppliedSize; i++) {
196 | deltas[i] = Math.random() > 0.5 ? 1 : -1;
197 | highArgs[i] = values[i] + ck * deltas[i];
198 | lowArgs[i] = values[i] - ck * deltas[i];
199 | }
200 |
201 | const lossDiff = this.loss(highArgs) - this.loss(lowArgs);
202 | for (let i = 0; i < filtersToBeAppliedSize; i++) {
203 | const g = (lossDiff / (2 * ck)) * deltas[i];
204 | const ak = a[i] / Math.pow(A + key + 1, alpha);
205 | values[i] = this.fixValueByFilterIDX(values[i] - ak * g, i);
206 | }
207 |
208 | const loss = this.loss(values);
209 | if (loss < bestLoss) {
210 | best = values.slice(0);
211 | bestLoss = loss;
212 | }
213 | }
214 |
215 | return { values: best, loss: bestLoss, called } as SPSAPayload;
216 | }
217 |
218 | /**
219 | * Checks how much is the loss for the filter in RGB and HSL colors
220 | *
221 | * @private
222 | * @param {SPSAPayload['values']} filters
223 | * @returns {number}
224 | * @memberof Solver
225 | */
226 | private loss(filters: SPSAPayload['values']): number {
227 | // Argument as an Array of percentages.
228 | const color = this.reusedColor;
229 |
230 | // Resetting the color to black in case
231 | // it was called more than once
232 | color.set(0, 0, 0);
233 |
234 | color.invert(filters[0] / 100);
235 | color.sepia(filters[1] / 100);
236 | color.saturate(filters[2] / 100);
237 | color.hueRotate(filters[3] * 3.6);
238 | color.brightness(filters[4] / 100);
239 | color.contrast(filters[5] / 100);
240 |
241 | const colorHSL = color.hsl();
242 |
243 | return (
244 | Math.abs(color.r - this.target.r) +
245 | Math.abs(color.g - this.target.g) +
246 | Math.abs(color.b - this.target.b) +
247 | Math.abs(colorHSL.h - this.targetHSL.h) +
248 | Math.abs(colorHSL.s - this.targetHSL.s) +
249 | Math.abs(colorHSL.l - this.targetHSL.l)
250 | );
251 | }
252 |
253 | /**
254 | * Returns the CSS filter list for the received HEX color
255 | *
256 | * @private
257 | * @param {number[]} filters
258 | * @returns {string}
259 | * @memberof Solver
260 | */
261 | private css(filters: number[]): string {
262 | const formatCssFilterValueByMultiplier = (idx: number, multiplier = 1): number =>
263 | Math.round(filters[idx] * multiplier);
264 |
265 | return [
266 | `invert(${formatCssFilterValueByMultiplier(0)}%)`,
267 | `sepia(${formatCssFilterValueByMultiplier(1)}%)`,
268 | `saturate(${formatCssFilterValueByMultiplier(2)}%)`,
269 | `hue-rotate(${formatCssFilterValueByMultiplier(3, 3.6)}deg)`,
270 | `brightness(${formatCssFilterValueByMultiplier(4)}%)`,
271 | `contrast(${formatCssFilterValueByMultiplier(5)}%)`,
272 | ].join(' ');
273 | }
274 | }
275 |
276 | export { Solver };
277 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc",
5 | "types": ["jest", "node"],
6 | "module": "esnext",
7 | "target": "es2015",
8 | "sourceMap": true /* Generates corresponding '.map' file. */
9 | },
10 | "files": [],
11 | "include": ["scripts/**/*", "src/**/*", "src/__tests__/**/**/*.ts", "src/**/**/*.d.ts"],
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "allowJs": false,
5 | // "strict": true,
6 | "allowSyntheticDefaultImports": true,
7 | // "declaration": true,
8 | "esModuleInterop": true,
9 | "experimentalDecorators": true,
10 | "downlevelIteration": true,
11 | // https://github.com/angular/angular-cli/issues/13886#issuecomment-471927518
12 | "importHelpers": true,
13 | "moduleResolution": "node",
14 | "noFallthroughCasesInSwitch": true,
15 | "noUnusedLocals": true,
16 | // "sourceMap": true,
17 | // "esModuleInterop": true,
18 | "baseUrl": "./",
19 | /* Basic Options */
20 | "module": "commonjs",
21 | // "module": "esnext",
22 | "lib": ["es2018", "dom", "es5", "scripthost"],
23 | "emitDecoratorMetadata": true,
24 | "typeRoots": ["node_modules/@types"],
25 | // "lib": ["es2018", "dom"],
26 | "target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */,
27 | "declaration": true /* Generates corresponding '.d.ts' file. */,
28 | "sourceMap": false /* Generates corresponding '.map' file. */,
29 | "outDir": "lib" /* Redirect output structure to the directory. */,
30 | "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
31 | "removeComments": false /* Do not emit comments to output. */,
32 | /* Strict Type-Checking Options */
33 | "strict": true /* Enable all strict type-checking options. */,
34 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
35 | "paths": {
36 | "*": ["src/entrypoints/*"]
37 | }
38 | },
39 | "include": ["src/**/*"],
40 | "exclude": ["node_modules", "src/__tests__/**/**/*.ts"]
41 | }
42 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc",
5 | "types": ["jest", "node"],
6 | "module": "esnext",
7 | "target": "es2015",
8 | "sourceMap": true /* Generates corresponding '.map' file. */
9 | },
10 | "files": [],
11 | "include": ["src/__tests__/**/**/*.ts", "src/**/**/*.d.ts"],
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------