├── .editorconfig ├── .eslintrc ├── .github └── workflows │ ├── codecov.yml │ ├── codeql.yml │ └── nodejs.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json ├── rollup.config.js └── test ├── hex.js ├── hsl.js ├── keywords.js ├── misc.js └── rgb.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.js] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "rollup" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | on: push 3 | jobs: 4 | coverage: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: actions/setup-node@v3 9 | with: 10 | node-version: '16' 11 | - run: npm ci 12 | - run: npm run coverage 13 | - name: Upload coverage to Codecov 14 | uses: codecov/codecov-action@v2 15 | with: 16 | token: ${{ secrets.CODECOV_TOKEN }} 17 | name: codecov-umbrella 18 | file: ./coverage/lcov.info 19 | fail_ci_if_error: true 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "30 0 * * 5" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | on: [push] 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: '16' 14 | - run: npm ci 15 | - run: npm run build --if-present 16 | - run: npm test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | .nyc_output 5 | coverage 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.2.0] - [#9](https://github.com/noeldelgado/mix-css-color/pull/9) - 2022-03-18 6 | ### Added 7 | - Type definitions 8 | 9 | ## [0.1.1] - [#1](https://github.com/noeldelgado/mix-css-color/pull/1) - 2020-05-19 10 | ### Added 11 | - more tests 12 | - input (empty, null, undefined) [a3e470b](https://github.com/noeldelgado/mix-css-color/commit/a3e470be08b02274ca6abf17c90444016d014995) 13 | - mix percentage (clamp 0...100) [53f1006](https://github.com/noeldelgado/mix-css-color/commit/53f1006bcdd258dfea3553c8a3d145eec17e3e00) 14 | - unpkg CDN [98637ed](https://github.com/noeldelgado/mix-css-color/commit/98637edcc53c05f37e945527f912ce42606d5416) 15 | - jsDelivr CDN [132cfb9](https://github.com/noeldelgado/mix-css-color/commit/132cfb9f572bebe9aa527a34b7ec43f823da1d9d) 16 | - related projects [37be7d5](https://github.com/noeldelgado/mix-css-color/commit/37be7d516a6e86eed409c6aa5a93b11a11bc8174) 17 | - CHANGELOG [77bbe53](https://github.com/noeldelgado/mix-css-color/commit/77bbe538a232cd5d2b3f149cedae922a6af1222b) 18 | 19 | ### Changed 20 | - parse-css-color@0.1.2 [5a0937e](https://github.com/noeldelgado/mix-css-color/commit/5a0937e6558f90457189882bfaddf144c10b6586) 21 | - move distributed files to dist folder [5caa083](https://github.com/noeldelgado/mix-css-color/commit/5caa083f1eb3c786b63db7e27e0f809b62411806) 22 | 23 | ### Fixed 24 | - clamp percentage 0...100 [ff3fc88](https://github.com/noeldelgado/mix-css-color/commit/ff3fc882232cb0b509ba23654bdc6f4a2bf1eca7) 25 | 26 | ### Removed 27 | - lib files from source code [b8cf130](https://github.com/noeldelgado/mix-css-color/commit/b8cf130e8802e71171a8cd938880e9e455a9d21f) 28 | 29 | ## [0.1.0] - 2020-05-18 30 | ### Added 31 | - support for: 32 | - #RGB|A, #RRGGBB|AA 33 | - RGB|A module level 3 and 4 34 | - HSL|A module level 3 and 4 35 | - color keywords 36 | - transparent 37 | 38 | [Unreleased]: https://github.com/noeldelgado/mix-css-color/compare/v0.2.0...HEAD 39 | [0.2.0]: https://github.com/noeldelgado/mix-css-color/compare/v0.1.1...v0.2.0 40 | [0.1.1]: https://github.com/noeldelgado/mix-css-color/compare/v0.1.0...v0.1.1 41 | [0.1.0]: https://github.com/noeldelgado/mix-css-color/releases/tag/v0.1.0 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Noel Delgado (pixelia.me) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mix-css-color 2 | [![npm version][npm-img]][npm-url] 3 | ![](https://img.badgesize.io/noeldelgado/mix-css-color/master/index.js.svg?compression=gzip) 4 | [![License][license-img]][license-url] 5 | [![codecov][codecov-image]][codecov-url] 6 | [![Known Vulnerabilities][snyk-img]][snyk-url] 7 | [![Libraries.io dependency status for latest release][librariesio-img]][librariesio-url] 8 | [![Total alerts][lgtm-image]][lgtm-url] 9 | [![Language grade: JavaScript][lgtm-grade-image]][lgtm-grade-url] 10 | 11 | Mix two colors together in variable proportion. Opacity is included in the calculations. 12 | 13 | _Output should be similar to the `less`/`sass` `mix()` function._ 14 | 15 | ### Supports 16 | * \ 17 | * Hexadecimal RGB value: #RGB #RRGGBB 18 | * #RGBA #RRGGBBAA (4 and 8-digit hexadecimal RGBA notation) 19 | * RGB/A color module level 3 and 4 (number, percentage) 20 | * HSL/A color module level 3 and 4 (number, deg, rad, turn) 21 | * \ 22 | * One of the [pre-defined color keywords](https://www.w3.org/wiki/CSS/Properties/color/keywords). 23 | * transparent 24 | * Shorthand for transparent black, rgba(0,0,0,0). 25 | 26 | ### Does not support 27 | * currentColor 28 | * inherit 29 | 30 | ## Installation 31 | 32 | **NPM** 33 | 34 | ```sh 35 | npm i mix-css-color 36 | ``` 37 | 38 | Or as a ` 44 | ``` 45 | 46 | **jsDelivr CDN** 47 | 48 | ```html 49 | 50 | ``` 51 | 52 | ## Usage 53 | ```js 54 | import mix from 'mix-css-color' 55 | 56 | mix('rgb(255 255 255 / 1)', 'red') // default 50% mix 57 | //> {rgba: [255, 128, 128, 1], hsla: [0, 100, 75, 1], hex: '#ff8080', hexa: '#ff8080ff' } 58 | 59 | mix('black', 'rgba(255, 0, 0, 0.22)', 22) // 22% mix 60 | //> { rgba: [78, 0, 0, 0.3916], hsla: [0, 100, 15, 0.3916], hex: '#4e0000', hexa: '#4e000064' } 61 | 62 | mix('rgba(100% 255 100% / 0)') // error: mixed percetange with integer 63 | //> null 64 | ``` 65 | See [tests](https://github.com/noeldelgado/mix-css-color/tree/master/test) for more cases. 66 | 67 | ## API 68 | ### mix(color1:string, color2: string, [percentage: number=50]) 69 | - @param color1 - CSS string 70 | - @param color2 - CSS string 71 | - @param [percentage=50] - a number within 0 and 100 72 | 73 | ## Dev 74 | ```sh 75 | npm install # install dependencies 76 | npm test # run the tests 77 | npm run dev # watch for changes and rebuild 78 | ``` 79 | 80 | ## Related 81 | - [parse-css-color](https://github.com/noeldelgado/parse-css-color) - Parse a CSS color string 82 | - [values.js](https://github.com/noeldelgado/values.js) - Get the tints and shades of a CSS color 83 | 84 | ## License 85 | MIT © [Noel Delgado](http://pixelia.me/) 86 | 87 | [npm-img]: https://img.shields.io/npm/v/mix-css-color.svg?logo=npm&label=NPM 88 | [license-img]: https://img.shields.io/npm/l/mix-css-color 89 | [license-url]: https://github.com/noeldelgado/mix-css-color/blob/master/LICENSE 90 | [codecov-image]: https://codecov.io/gh/noeldelgado/mix-css-color/branch/master/graph/badge.svg 91 | [codecov-url]: https://codecov.io/gh/noeldelgado/mix-css-color 92 | [npm-url]: https://www.npmjs.com/package/mix-css-color 93 | [snyk-img]: https://snyk.io/test/npm/mix-css-color/badge.svg 94 | [snyk-url]: https://snyk.io/test/npm/mix-css-color 95 | [librariesio-img]: https://img.shields.io/librariesio/release/npm/mix-css-color.svg?logo=librariesdotio 96 | [librariesio-url]: https://libraries.io/npm/mix-css-color 97 | [lgtm-image]: https://img.shields.io/lgtm/alerts/g/noeldelgado/mix-css-color.svg?logo=lgtm&logoWidth=18 98 | [lgtm-url]: https://lgtm.com/projects/g/noeldelgado/mix-css-color/alerts/ 99 | [lgtm-grade-image]: https://img.shields.io/lgtm/grade/javascript/g/noeldelgado/mix-css-color.svg?logo=lgtm&logoWidth=18 100 | [lgtm-grade-url]: https://lgtm.com/projects/g/noeldelgado/mix-css-color/context:javascript -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "mix-css-color" { 2 | export type RgbaTuple = [ 3 | red: number, 4 | green: number, 5 | blue: number, 6 | alpha: number 7 | ]; 8 | 9 | export type HslaTuple = [ 10 | hue: number, 11 | saturation: number, 12 | lightness: number, 13 | alpha: number 14 | ]; 15 | 16 | export type MixObject = { 17 | rgba: RgbaTuple, 18 | hsla: HslaTuple, 19 | hex: string, 20 | hexa: string 21 | } 22 | 23 | export default function mix(c1: string, c2: string, percentage?: number): MixObject 24 | } 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * mix-css-color 3 | * @version __VERSION__ 4 | * @link http://github.com/noeldelgado/mix-css-color/ 5 | * @license MIT 6 | */ 7 | import parse from 'parse-css-color'; 8 | import hsl2rgb from 'pure-color/convert/hsl2rgb'; 9 | import rgb2hex from 'pure-color/convert/rgb2hex'; 10 | import rgb2hsl from 'pure-color/convert/rgb2hsl'; 11 | 12 | function parseColor(color) { 13 | const res = parse(color); 14 | if (res === null) return null; 15 | if (res.type === 'hsl') res.values = hsl2rgb(res.values); 16 | return res; 17 | } 18 | 19 | /** 20 | * Mix two colors together in variable proportion. Opacity is included in the calculations. 21 | * Copyright (c) 2006-2009 Hampton Catlin, Natalie Weizenbaum, and Chris Eppstein 22 | * http://sass-lang.com 23 | * @see https://github.com/less/less.js/blob/cae5021358a5fca932c32ed071f652403d07def8/lib/less/functions/color.js#L302 24 | */ 25 | export default function mix(color1, color2, percentage = 50) { 26 | const c1 = parseColor(color1); 27 | const c2 = parseColor(color2); 28 | 29 | if (!c1 || !c2) return null; 30 | 31 | const p = Math.min(Math.max(0, percentage), 100) / 100.0; 32 | const w = p * 2 - 1; 33 | const a = c1.alpha - c2.alpha; 34 | const w1 = ((w * a === -1 ? w : (w + a) / (1 + w * a)) + 1) / 2.0; 35 | const w2 = 1 - w1; 36 | const [r, g, b] = c1.values.map((c, i) => Math.round(c1.values[i] * w1 + c2.values[i] * w2)); 37 | const alpha = parseFloat((c1.alpha * p + c2.alpha * (1 - p)).toFixed(8)); 38 | 39 | return { 40 | hex: rgb2hex([r, g, b]), 41 | hexa: rgb2hex([r, g, b, alpha]), 42 | rgba: [r, g, b, alpha], 43 | hsla: [...rgb2hsl([r, g, b]).map(Math.round), alpha] 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mix-css-color", 3 | "version": "0.2.0", 4 | "description": "Mix two colors together in variable proportion. Opacity is included in the calculations.", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.esm.js", 7 | "browser": "dist/index.umd.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "dev": "rollup -c -w", 14 | "test": "tape -r esm test/*.js | tap-spec", 15 | "build": "rollup -c", 16 | "watch": "onchange dist/index.cjs.js test/*.js -- npm test", 17 | "coverage": "nyc npm run test && nyc report --reporter=lcov", 18 | "prepublishOnly": "npm run build && npm run test" 19 | }, 20 | "keywords": [ 21 | "mix", 22 | "css", 23 | "color", 24 | "hex", 25 | "rgb", 26 | "hsl" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/noeldelgado/mix-css-color.git" 31 | }, 32 | "author": "Noel Delgado (https://pixelia.me/)", 33 | "license": "MIT", 34 | "dependencies": { 35 | "parse-css-color": "^0.1.2", 36 | "pure-color": "^1.3.0" 37 | }, 38 | "devDependencies": { 39 | "@rollup/plugin-buble": "^0.21.3", 40 | "@rollup/plugin-commonjs": "^11.1.0", 41 | "@rollup/plugin-node-resolve": "^7.1.3", 42 | "@rollup/plugin-replace": "^2.3.2", 43 | "eslint": "^7.0.0", 44 | "eslint-config-rollup": "^2.0.4", 45 | "esm": "^3.2.25", 46 | "nyc": "^15.1.0", 47 | "onchange": "^7.0.2", 48 | "rollup": "^2.10.2", 49 | "rollup-plugin-copy": "^3.4.0", 50 | "rollup-plugin-terser": "^7.0.2", 51 | "tap-spec": "^5.0.0", 52 | "tape": "^5.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import buble from '@rollup/plugin-buble'; 4 | import replace from '@rollup/plugin-replace'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import copy from 'rollup-plugin-copy'; 7 | 8 | import pkg from './package.json'; 9 | 10 | export default [ 11 | { 12 | input: 'index.js', 13 | output: { 14 | file: pkg.browser, 15 | format: 'umd', 16 | name: 'mixCssColor' 17 | }, 18 | plugins: [ 19 | resolve(), 20 | commonjs(), 21 | buble(), 22 | replace({ __VERSION__: `v${pkg.version}` }), 23 | terser(), 24 | copy({ 25 | targets: [ 26 | { 27 | src: 'index.d.ts', 28 | dest: 'dist', 29 | } 30 | ] 31 | }) 32 | ] 33 | }, 34 | { 35 | input: 'index.js', 36 | external: [ 37 | 'parse-css-color', 38 | 'pure-color/convert/hsl2rgb', 39 | 'pure-color/convert/rgb2hex', 40 | 'pure-color/convert/rgb2hsl' 41 | ], 42 | plugins: [resolve(), commonjs(), replace({ __VERSION__: `v${pkg.version}` })], 43 | output: [ 44 | { file: pkg.main, format: 'cjs' }, 45 | { file: pkg.module, format: 'es' } 46 | ] 47 | } 48 | ]; 49 | -------------------------------------------------------------------------------- /test/hex.js: -------------------------------------------------------------------------------- 1 | import tape from 'tape'; 2 | 3 | import mix from '..'; 4 | 5 | tape('hex', (t) => { 6 | const { deepEqual, equal, end } = t; 7 | 8 | equal(mix('fff', '#fff'), null, 'missing token fff'); 9 | equal(mix('##fff', '#fff'), null, 'double token ##fff'); 10 | equal(mix(' #fff', '#fff'), null, 'leading whitespace " #fff"'); 11 | equal(mix('#fff ', '#fff'), null, 'trailing whitespace "#fff "'); 12 | equal(mix('#y3', '#fff'), null, 'invalid hex #y3'); 13 | equal(mix('#ff', '#fff'), null, 'invalid hex #ff'); 14 | equal(mix('#ffg', '#fff'), null, 'invalid hex #ffg'); 15 | 16 | deepEqual( 17 | mix('#00000000', '#00000000'), 18 | { 19 | rgba: [0, 0, 0, 0], 20 | hsla: [0, 0, 0, 0], 21 | hex: '#000000', 22 | hexa: '#00000000' 23 | }, 24 | '#000000000 -> #00000000 : 50' 25 | ); 26 | 27 | deepEqual( 28 | mix('#ff6347', '#0ff'), 29 | { 30 | rgba: [128, 177, 163, 1], 31 | hsla: [163, 24, 60, 1], 32 | hex: '#80b1a3', 33 | hexa: '#80b1a3ff' 34 | }, 35 | '#ff6347 -> #0ff : 50' 36 | ); 37 | 38 | deepEqual( 39 | mix('#ff6347', '#0ff', 100), 40 | { 41 | rgba: [255, 99, 71, 1], 42 | hsla: [9, 100, 64, 1], 43 | hex: '#ff6347', 44 | hexa: '#ff6347ff' 45 | }, 46 | '#ff6347 -> #0ff : 100' 47 | ); 48 | 49 | deepEqual( 50 | mix('#ff6347', '#0ff', 200), 51 | { 52 | rgba: [255, 99, 71, 1], 53 | hsla: [9, 100, 64, 1], 54 | hex: '#ff6347', 55 | hexa: '#ff6347ff' 56 | }, 57 | '#ff6347 -> #0ff : 200 (clipped to 100)' 58 | ); 59 | 60 | deepEqual( 61 | mix('#ff6347', '#0ff', 0), 62 | { 63 | rgba: [0, 255, 255, 1], 64 | hsla: [180, 100, 50, 1], 65 | hex: '#00ffff', 66 | hexa: '#00ffffff' 67 | }, 68 | '#ff6347 -> #0ff : 0' 69 | ); 70 | 71 | deepEqual( 72 | mix('#ff6347', '#0ff', -100), 73 | { 74 | rgba: [0, 255, 255, 1], 75 | hsla: [180, 100, 50, 1], 76 | hex: '#00ffff', 77 | hexa: '#00ffffff' 78 | }, 79 | '#ff6347 -> #0ff : -100 (clipped to 0)' 80 | ); 81 | 82 | deepEqual( 83 | mix('#fff', '#f00'), 84 | { 85 | rgba: [255, 128, 128, 1], 86 | hsla: [0, 100, 75, 1], 87 | hex: '#ff8080', 88 | hexa: '#ff8080ff' 89 | }, 90 | '#fff -> #f00 : 50' 91 | ); 92 | 93 | deepEqual( 94 | mix('#ff6347', '#0099ff80', 42), 95 | { 96 | rgba: [174, 116, 129, 0.71113725], 97 | hsla: [347, 26, 57, 0.71113725], 98 | hex: '#ae7481', 99 | hexa: '#ae7481b5' 100 | }, 101 | '#ff6347 -> #0099ff80 : 42' 102 | ); 103 | 104 | deepEqual( 105 | mix('#000', 'rgba(255, 0, 0, 0.22)', 22), 106 | { 107 | rgba: [78, 0, 0, 0.3916], 108 | hsla: [0, 100, 15, 0.3916], 109 | hex: '#4e0000', 110 | hexa: '#4e000064' 111 | }, 112 | '#000 -> rgba(255, 0, 0, 0.22) : 22' 113 | ); 114 | 115 | deepEqual( 116 | mix('#ff6347', 'rgb(0 255 255 / 0.5)', 80), 117 | { 118 | rgba: [235, 111, 85, 0.9], 119 | hsla: [10, 79, 63, 0.9], 120 | hex: '#eb6f55', 121 | hexa: '#eb6f55e6' 122 | }, 123 | '#ff6347 -> rgb(0 255 255 / 0.5) : 80' 124 | ); 125 | 126 | deepEqual( 127 | mix('#ff6347', 'hsla(360 100% 50% / 0.8)', 40), 128 | { 129 | rgba: [255, 50, 36, 0.88], 130 | hsla: [4, 100, 57, 0.88], 131 | hex: '#ff3224', 132 | hexa: '#ff3224e0' 133 | }, 134 | '#ff6347 -> hsla(360 100% 50% / 0.8) : 40' 135 | ); 136 | 137 | end(); 138 | }); 139 | -------------------------------------------------------------------------------- /test/hsl.js: -------------------------------------------------------------------------------- 1 | import tape from 'tape'; 2 | 3 | import mix from '..'; 4 | 5 | tape('hsl', (t) => { 6 | const { deepEqual, equal, end } = t; 7 | 8 | equal(mix('hsl()', '#fff'), null, 'missing channels'); 9 | 10 | deepEqual( 11 | mix('hsl(0 0% 0% / 0)', 'hsl(0 0% 0% / 0)'), 12 | { 13 | rgba: [0, 0, 0, 0], 14 | hsla: [0, 0, 0, 0], 15 | hex: '#000000', 16 | hexa: '#00000000' 17 | }, 18 | 'transparent -> transparent : default weight 50' 19 | ); 20 | 21 | deepEqual( 22 | mix('hsl(9, 100%, 64%)', 'hsl(180, 100%, 50%)'), 23 | { 24 | rgba: [128, 177, 163, 1], 25 | hsla: [163, 24, 60, 1], 26 | hex: '#80b1a3', 27 | hexa: '#80b1a3ff' 28 | }, 29 | 'hsl(9, 100%, 64%) -> hsl(180, 100%, 50%) : 50' 30 | ); 31 | 32 | deepEqual( 33 | mix('hsl(9, 100%, 64%)', 'hsl(180, 100%, 50%)', 100), 34 | { 35 | rgba: [255, 99, 71, 1], 36 | hsla: [9, 100, 64, 1], 37 | hex: '#ff6347', 38 | hexa: '#ff6347ff' 39 | }, 40 | 'hsl(9, 100%, 64%) -> hsl(180, 100%, 50%) : 100' 41 | ); 42 | 43 | deepEqual( 44 | mix('hsl(9, 100%, 64%)', 'hsl(180, 100%, 50%)', 200), 45 | { 46 | rgba: [255, 99, 71, 1], 47 | hsla: [9, 100, 64, 1], 48 | hex: '#ff6347', 49 | hexa: '#ff6347ff' 50 | }, 51 | 'hsl(9, 100%, 64%) -> hsl(180, 100%, 50%) : 200 (clipped to 100)' 52 | ); 53 | 54 | deepEqual( 55 | mix('hsl(9, 100%, 64%)', 'hsl(180, 100%, 50%)', 0), 56 | { 57 | rgba: [0, 255, 255, 1], 58 | hsla: [180, 100, 50, 1], 59 | hex: '#00ffff', 60 | hexa: '#00ffffff' 61 | }, 62 | 'hsl(9, 100%, 64%) -> hsl(180, 100%, 50%) : 0' 63 | ); 64 | 65 | deepEqual( 66 | mix('hsl(9, 100%, 64%)', 'hsl(180, 100%, 50%)', -100), 67 | { 68 | rgba: [0, 255, 255, 1], 69 | hsla: [180, 100, 50, 1], 70 | hex: '#00ffff', 71 | hexa: '#00ffffff' 72 | }, 73 | 'hsl(9, 100%, 64%) -> hsl(180, 100%, 50%) : -100 (clipped to 0)' 74 | ); 75 | 76 | deepEqual( 77 | mix('hsl(9, 100%, 64%)', '#0099ff80', 42), 78 | { 79 | rgba: [174, 116, 129, 0.71113725], 80 | hsla: [347, 26, 57, 0.71113725], 81 | hex: '#ae7481', 82 | hexa: '#ae7481b5' 83 | }, 84 | 'hsl(9, 100%, 64%) -> #0099ff80 : 42' 85 | ); 86 | 87 | deepEqual( 88 | mix('hsl(0 0% 0% / 1)', 'rgba(255, 0, 0, 0.22)', 22), 89 | { 90 | rgba: [78, 0, 0, 0.3916], 91 | hsla: [0, 100, 15, 0.3916], 92 | hex: '#4e0000', 93 | hexa: '#4e000064' 94 | }, 95 | 'hsl(0 0% 0% / 1) -> rgba(255, 0, 0, 0.22) : 22' 96 | ); 97 | 98 | deepEqual( 99 | mix('hsl(9 100% 64%)', 'rgb(0 255 255 / 0.5)', 80), 100 | { 101 | rgba: [235, 111, 86, 0.9], 102 | hsla: [10, 79, 63, 0.9], 103 | hex: '#eb6f56', 104 | hexa: '#eb6f56e6' 105 | }, 106 | 'hsl(9 100% 64%) -> rgb(0 255 255 / 0.5) : 80' 107 | ); 108 | 109 | deepEqual( 110 | mix('hsl(0 0% 100% / 1)', 'hsl(0 100% 50% / 1)'), 111 | { 112 | rgba: [255, 128, 128, 1], 113 | hsla: [0, 100, 75, 1], 114 | hex: '#ff8080', 115 | hexa: '#ff8080ff' 116 | }, 117 | 'hsl(0 0% 100% / 1) -> hsl(0 100% 50% / 1) : 50' 118 | ); 119 | 120 | deepEqual( 121 | mix('hsl(9 100% 64%)', 'hsla(360 100% 50% / 0.8)', 40), 122 | { 123 | rgba: [255, 49, 36, 0.88], 124 | hsla: [4, 100, 57, 0.88], 125 | hex: '#ff3124', 126 | hexa: '#ff3124e0' 127 | }, 128 | 'hsl(9 100% 64%) -> hsla(360 100% 50% / 0.8) : 40' 129 | ); 130 | 131 | end(); 132 | }); 133 | -------------------------------------------------------------------------------- /test/keywords.js: -------------------------------------------------------------------------------- 1 | import tape from 'tape'; 2 | 3 | import mix from '..'; 4 | 5 | tape('keywords', (t) => { 6 | const { deepEqual, end, equal } = t; 7 | 8 | equal(mix('foo', 'red'), null, 'invalid foo'); 9 | equal(mix('red', 'bar'), null, 'invalid bar'); 10 | equal(mix('re', 'red'), null, 'invalid re'); 11 | equal(mix(' red ', 'red'), null, 'leading whitespace "red "'); 12 | equal(mix('red ', 'red'), null, 'trailing whitespace "red "'); 13 | 14 | deepEqual( 15 | mix('transparent', 'transparent'), 16 | { 17 | rgba: [0, 0, 0, 0], 18 | hsla: [0, 0, 0, 0], 19 | hex: '#000000', 20 | hexa: '#00000000' 21 | }, 22 | 'transparent -> transparent : 50' 23 | ); 24 | 25 | deepEqual( 26 | mix('tomato', 'cyan'), 27 | { 28 | rgba: [128, 177, 163, 1], 29 | hsla: [163, 24, 60, 1], 30 | hex: '#80b1a3', 31 | hexa: '#80b1a3ff' 32 | }, 33 | 'tomato -> cyan : 50' 34 | ); 35 | 36 | deepEqual( 37 | mix('tomato', 'cyan', 100), 38 | { 39 | rgba: [255, 99, 71, 1], 40 | hsla: [9, 100, 64, 1], 41 | hex: '#ff6347', 42 | hexa: '#ff6347ff' 43 | }, 44 | 'tomato -> cyan : 100' 45 | ); 46 | 47 | deepEqual( 48 | mix('tomato', 'cyan', 200), 49 | { 50 | rgba: [255, 99, 71, 1], 51 | hsla: [9, 100, 64, 1], 52 | hex: '#ff6347', 53 | hexa: '#ff6347ff' 54 | }, 55 | 'tomato -> cyan : 200 (clipped to 100)' 56 | ); 57 | 58 | deepEqual( 59 | mix('tomato', 'cyan', 0), 60 | { 61 | rgba: [0, 255, 255, 1], 62 | hsla: [180, 100, 50, 1], 63 | hex: '#00ffff', 64 | hexa: '#00ffffff' 65 | }, 66 | 'tomato -> cyan : 0' 67 | ); 68 | 69 | deepEqual( 70 | mix('tomato', 'cyan', -100), 71 | { 72 | rgba: [0, 255, 255, 1], 73 | hsla: [180, 100, 50, 1], 74 | hex: '#00ffff', 75 | hexa: '#00ffffff' 76 | }, 77 | 'tomato -> cyan : -100 (clipped to 0)' 78 | ); 79 | 80 | deepEqual( 81 | mix('tomato', '#0099ff80', 42), 82 | { 83 | rgba: [174, 116, 129, 0.71113725], 84 | hsla: [347, 26, 57, 0.71113725], 85 | hex: '#ae7481', 86 | hexa: '#ae7481b5' 87 | }, 88 | 'tomato -> #0099ff80 : 42' 89 | ); 90 | 91 | deepEqual( 92 | mix('black', 'rgba(255, 0, 0, 0.22)', 22), 93 | { 94 | rgba: [78, 0, 0, 0.3916], 95 | hsla: [0, 100, 15, 0.3916], 96 | hex: '#4e0000', 97 | hexa: '#4e000064' 98 | }, 99 | 'black -> rgba(255, 0, 0, 0.22) : 22' 100 | ); 101 | 102 | deepEqual( 103 | mix('tomato', 'rgb(0 255 255 / 0.5)', 80), 104 | { 105 | rgba: [235, 111, 85, 0.9], 106 | hsla: [10, 79, 63, 0.9], 107 | hex: '#eb6f55', 108 | hexa: '#eb6f55e6' 109 | }, 110 | 'tomato -> rgb(0 255 255 / 0.5) : 80' 111 | ); 112 | 113 | deepEqual( 114 | mix('tomato', 'hsla(360, 100%, 50%, 0.8)', 40), 115 | { 116 | rgba: [255, 50, 36, 0.88], 117 | hsla: [4, 100, 57, 0.88], 118 | hex: '#ff3224', 119 | hexa: '#ff3224e0' 120 | }, 121 | 'tomato -> hsla(360, 100%, 50%, 0.8) : 40' 122 | ); 123 | 124 | end(); 125 | }); 126 | -------------------------------------------------------------------------------- /test/misc.js: -------------------------------------------------------------------------------- 1 | import tape from 'tape'; 2 | 3 | import mix from '..'; 4 | 5 | tape('misc', (t) => { 6 | const { equal, end } = t; 7 | 8 | equal(mix(), null); 9 | equal(mix(''), null); 10 | equal(mix(undefined), null); // eslint-disable-line 11 | equal(mix(null), null); // eslint-disable-line 12 | equal(mix('', ''), null); 13 | equal(mix('', 'red'), null); 14 | equal(mix('red', ''), null); 15 | equal(mix(undefined, 'red'), null); // eslint-disable-line 16 | equal(mix('red', undefined), null); // eslint-disable-line 17 | equal(mix(undefined, undefined), null); // eslint-disable-line 18 | equal(mix(null, 'red'), null); 19 | equal(mix(null), null); 20 | equal(mix('currentColor', 'red'), null); 21 | equal(mix('inherit', 'red'), null); 22 | 23 | end(); 24 | }); 25 | -------------------------------------------------------------------------------- /test/rgb.js: -------------------------------------------------------------------------------- 1 | import tape from 'tape'; 2 | 3 | import mix from '..'; 4 | 5 | tape('rgb', (t) => { 6 | const { deepEqual, equal, end } = t; 7 | 8 | equal(mix('rgb()', '#fff'), null, 'missing channels'); 9 | 10 | deepEqual( 11 | mix('rgb(0 0 0 / 0)', 'rgb(0 0 0 / 0)'), 12 | { 13 | rgba: [0, 0, 0, 0], 14 | hsla: [0, 0, 0, 0], 15 | hex: '#000000', 16 | hexa: '#00000000' 17 | }, 18 | 'rgb(0 0 0 / 0) -> rgb(0 0 0 / 0) : 50' 19 | ); 20 | 21 | deepEqual( 22 | mix('rgb(255 99 71 / 1)', 'rgb(0 255 255)'), 23 | { 24 | rgba: [128, 177, 163, 1], 25 | hsla: [163, 24, 60, 1], 26 | hex: '#80b1a3', 27 | hexa: '#80b1a3ff' 28 | }, 29 | 'rgb(255 99 71 / 1) -> rgb(0 255 255) : 50' 30 | ); 31 | 32 | deepEqual( 33 | mix('rgb(255 99 71 / 1)', 'rgb(0 255 255)', 100), 34 | { 35 | rgba: [255, 99, 71, 1], 36 | hsla: [9, 100, 64, 1], 37 | hex: '#ff6347', 38 | hexa: '#ff6347ff' 39 | }, 40 | 'rgb(255 99 71 / 1) -> rgb(0 255 255) : 100' 41 | ); 42 | 43 | deepEqual( 44 | mix('rgb(255 99 71 / 1)', 'rgb(0 255 255)', 200), 45 | { 46 | rgba: [255, 99, 71, 1], 47 | hsla: [9, 100, 64, 1], 48 | hex: '#ff6347', 49 | hexa: '#ff6347ff' 50 | }, 51 | 'rgb(255 99 71 / 1) -> rgb(0 255 255) : 200 (clipped to 100)' 52 | ); 53 | 54 | deepEqual( 55 | mix('rgb(255 99 71 / 1)', 'rgb(0 255 255)', 0), 56 | { 57 | rgba: [0, 255, 255, 1], 58 | hsla: [180, 100, 50, 1], 59 | hex: '#00ffff', 60 | hexa: '#00ffffff' 61 | }, 62 | 'rgb(255 99 71 / 1) -> rgb(0 255 255) : 0' 63 | ); 64 | 65 | deepEqual( 66 | mix('rgb(255 99 71 / 1)', 'rgb(0 255 255)', -100), 67 | { 68 | rgba: [0, 255, 255, 1], 69 | hsla: [180, 100, 50, 1], 70 | hex: '#00ffff', 71 | hexa: '#00ffffff' 72 | }, 73 | 'rgb(255 99 71 / 1) -> rgb(0 255 255) : -100 (clipped to 0)' 74 | ); 75 | 76 | deepEqual( 77 | mix('rgb(255 99 71 / 1)', '#0099ff80', 42), 78 | { 79 | rgba: [174, 116, 129, 0.71113725], 80 | hsla: [347, 26, 57, 0.71113725], 81 | hex: '#ae7481', 82 | hexa: '#ae7481b5' 83 | }, 84 | 'rgb(255 99 71 / 1) -> #0099ff80 : 42' 85 | ); 86 | 87 | deepEqual( 88 | mix('rgb(0, 0, 0)', 'rgba(255, 0, 0, 0.22)', 22), 89 | { 90 | rgba: [78, 0, 0, 0.3916], 91 | hsla: [0, 100, 15, 0.3916], 92 | hex: '#4e0000', 93 | hexa: '#4e000064' 94 | }, 95 | 'rgb(0, 0, 0) -> rgba(255, 0, 0, 0.22) : 22' 96 | ); 97 | 98 | deepEqual( 99 | mix('rgb(255 99 71 / 1)', 'rgb(0 255 255 / 0.5)', 80), 100 | { 101 | rgba: [235, 111, 85, 0.9], 102 | hsla: [10, 79, 63, 0.9], 103 | hex: '#eb6f55', 104 | hexa: '#eb6f55e6' 105 | }, 106 | 'rgb(255 99 71 / 1) -> rgb(0 255 255 / 0.5) : 80' 107 | ); 108 | 109 | deepEqual( 110 | mix('rgb(255 255 255 / 1)', 'rgb(255 0 0 / 1)'), 111 | { 112 | rgba: [255, 128, 128, 1], 113 | hsla: [0, 100, 75, 1], 114 | hex: '#ff8080', 115 | hexa: '#ff8080ff' 116 | }, 117 | 'rgb(255 255 255 / 1) -> rgb(255 0 0 / 1) : 50' 118 | ); 119 | 120 | deepEqual( 121 | mix('rgb(255, 255, 255, 100%)', 'hsl(360 100% 50% / 1)', 100), 122 | { 123 | rgba: [255, 255, 255, 1], 124 | hsla: [0, 0, 100, 1], 125 | hex: '#ffffff', 126 | hexa: '#ffffffff' 127 | }, 128 | 'rgb(255, 255, 255, 100%) -> hsl(360 100% 50% / 1) : 100' 129 | ); 130 | 131 | deepEqual( 132 | mix('rgb(255 99 71 / 1)', 'hsla(360 100% 50% / 0.8)', 40), 133 | { 134 | rgba: [255, 50, 36, 0.88], 135 | hsla: [4, 100, 57, 0.88], 136 | hex: '#ff3224', 137 | hexa: '#ff3224e0' 138 | }, 139 | 'rgb(255 99 71 / 1) -> hsla(360 100% 50% / 0.8) : 40' 140 | ); 141 | 142 | end(); 143 | }); 144 | --------------------------------------------------------------------------------