├── .github └── workflows │ └── publish-docs.yml ├── .gitignore ├── .npmignore ├── .sassdocrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── _index.scss ├── demo ├── _gradient.scss ├── index.html ├── style.css ├── style.css.map └── style.scss ├── package.json ├── sass ├── _blend.scss ├── adjust │ ├── _channels.scss │ ├── _from.scss │ ├── _index.scss │ ├── _parse.scss │ └── _utils.scss ├── cie │ ├── _api.scss │ ├── _index.scss │ └── _lch.scss ├── convert │ ├── _conversions.scss │ ├── _extra.scss │ ├── _index.scss │ └── _utilities.scss ├── inspect │ ├── _cie.scss │ ├── _contrast.scss │ └── _index.scss └── utils │ ├── _array.scss │ ├── _channel.scss │ ├── _matrix.scss │ ├── _pow.scss │ ├── _relative.scss │ ├── _throw.scss │ └── _units.scss ├── src ├── json.css ├── json.css.map └── json.scss ├── test ├── adjust │ ├── _channels.scss │ ├── _from.scss │ ├── _index.scss │ ├── _parse.scss │ └── _utils.scss ├── cie │ ├── _api.scss │ ├── _index.scss │ └── _lch.scss ├── convert │ ├── _conversions.scss │ ├── _extra.scss │ ├── _index.scss │ └── _utilities.scss ├── index.scss ├── inspect │ ├── _cie.scss │ ├── _contrast.scss │ └── _index.scss ├── test_sass.js └── utils │ ├── _array.scss │ ├── _channel.scss │ ├── _index.scss │ ├── _matrix.scss │ ├── _pow.scss │ ├── _relative.scss │ └── _units.scss └── yarn.lock /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish documentation 2 | on: 3 | release: # Run when stable releases are published 4 | types: [released] 5 | workflow_dispatch: # Run on-demand 6 | inputs: 7 | ref: 8 | description: Git ref to build docs from 9 | required: true 10 | default: main 11 | type: string 12 | 13 | jobs: 14 | push-branch: 15 | name: Build & push docs 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | steps: 22 | - name: Check out from release 23 | if: github.event_name == 'release' 24 | uses: actions/checkout@v3 25 | - name: Check out from manual input 26 | if: github.event_name == 'workflow_dispatch' 27 | uses: actions/checkout@v3 28 | with: 29 | ref: ${{ inputs.ref }} 30 | - uses: actions/setup-node@v3 31 | with: 32 | node-version: 'lts/*' 33 | cache: yarn 34 | - run: yarn install 35 | - run: yarn herman 36 | - name: Clone docs branch 37 | uses: actions/checkout@v3 38 | with: 39 | path: docs-branch 40 | ref: oddleventy-docs 41 | - name: Commit & push to docs branch 42 | run: | 43 | SHA=$(git rev-parse HEAD) 44 | cd docs-branch 45 | rm -rf blend/docs 46 | mkdir -p blend/docs 47 | cp -r ${{ github.workspace }}/docs/ blend/ 48 | git config user.name github-actions 49 | git config user.email github-actions@github.com 50 | git add -A . 51 | git commit --allow-empty \ 52 | -m "Update from https://github.com/${{ github.repository }}/commit/$SHA" \ 53 | -m "Full log: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" 54 | git push origin oddleventy-docs 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | .sass-cache 3 | .vscode 4 | docs 5 | node_modules 6 | .nvmrc 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # git-ignore 2 | **/.DS_Store 3 | .sass-cache 4 | .vscode 5 | docs 6 | node_modules 7 | .nvmrc 8 | 9 | # only npm-ignore 10 | demo 11 | -------------------------------------------------------------------------------- /.sassdocrc: -------------------------------------------------------------------------------- 1 | theme: herman 2 | dest: docs/ 3 | src: sass/ 4 | 5 | display: 6 | access: 7 | - public 8 | 9 | groups: 10 | Color Functions: 11 | cie-formats: Lab & LCH Formats 12 | contrast: Color Contrast 13 | adjust: Relative Colors 14 | cie-inspect: Inspecting Colors 15 | 16 | herman: 17 | sass: 18 | sassOptions: 19 | loadPaths: ['sass'] 20 | use: ['blend'] 21 | jsonFile: 'src/json.css' 22 | extraDocs: 23 | - name: 'Changelog' 24 | path: CHANGELOG.md 25 | - name: 'Contributing' 26 | path: CONTRIBUTING.md 27 | - name: 'License' 28 | path: LICENSE.md 29 | extraLinks: 30 | - name: 'LCH Color Picker' 31 | url: 'https://css.land/lch/' 32 | - name: 'CSS Colors Level 4' 33 | url: 'https://www.w3.org/TR/css-color-4/' 34 | - name: 'CSS Colors Level 5' 35 | url: 'https://www.w3.org/TR/css-color-5/' 36 | - name: 'GitHub Source' 37 | url: 'https://github.com/oddbird/blend/' 38 | - name: 'OddBird' 39 | url: 'https://www.oddbird.net/' 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Blend Changelog 2 | 3 | ## v0.2.4 - 2022.12.14 4 | 5 | - 🏠 INTERNAL: Remove documentation from npm package 6 | - 🏠 INTERNAL: Upgrade dependencies 7 | 8 | ## v0.2.3 - 2021.09.18 9 | 10 | - Correction to the sRGB to XYZ to sRGB matrices, improve round-tripping 11 | (based on [CSSWG update](https://github.com/w3c/csswg-drafts/issues/5922)) 12 | - Update to use `math.div()` in place of 13 | [slash as division](https://sass-lang.com/documentation/breaking-changes/slash-div) 14 | 15 | ## v0.2.2 - 2020.06.30 16 | 17 | - Documentation cleanup 18 | - Upgraded dev dependencies 19 | 20 | ## v0.2.1 - 2020.05.28 21 | 22 | - Updated license to The Hippocratic License 2.1 23 | - Documentation cleanup 24 | 25 | ## v0.2.0 - 2020.05.27 26 | 27 | - **BREAKING**: Moved project to [oddbird/blend][repo] on GitHub, 28 | and [@oddbird/blend][pkg] on NPM. 29 | - **BREAKING**: Remove over-complicated settings & output options for now. 30 | Focus on practical Sass conversion to and from CIE functions. 31 | - **NEW**: `lch()` hue channel accepts any angle unit 32 | (e.g. `turn`, `rad`, `grad`, or `deg`) 33 | - **NEW**: `lab($lab, $a)` returns an (sRGB) Sass color 34 | - **NEW**: Inspect LCH/Lab values of a Sass color with 35 | - `lightness()` 36 | - `a()` and `b()` 37 | - `chroma()` and `hue()` 38 | - **NEW**: `contrast()` selects the best contrast from a list 39 | - **NEW**: Generate new colors based on relative LCH & Lab adjustments: 40 | - `set()` to replace a channel value 41 | - `adjust()` to add or subtract from a channel 42 | - `scale()` to scale fluidly towards one "end" of the channel range 43 | - **NEW**: `from()` converts a Sass color to LCH 44 | in order to adjust CIE lightness, chroma, and hue -- 45 | using a syntax roughly based on 46 | [CSS Colors Level 5 relative syntax][relative] 47 | 48 | [pkg]: https://www.npmjs.com/package/@oddbird/blend 49 | [repo]: https://github.com/oddbird/blend/ 50 | [relative]: https://www.w3.org/TR/css-color-5/#relative-RGB 51 | 52 | ## v0.1.1 - 2020.05.06 53 | 54 | - **NEW**: `lch($lch, $a)` returns an (sRGB) Sass color 55 | - Various other now-removed things… 56 | ¯\\\_(ツ)\_/¯ that's what pre-releases are for 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Blend 2 | 3 | Ideas, issues, and pull-requests are welcome! 4 | 5 | - [**Github Issues**](https://github.com/oddbird/blend/issues/) 6 | are the best place to request a feature, 7 | file a bug, 8 | or just ask a question. 9 | Also a great place to discuss possible features 10 | before you submit a PR. 11 | - **Pull Requests** are a big help, 12 | if you're willing to jump in and make things happen. 13 | For a bugfix, or documentation, 14 | just jump right in. 15 | For major changes or new features, 16 | it's best to discuss in an issue first. 17 | 18 | ## Process 19 | 20 | Clone the repo, and then use yarn to install development dependencies 21 | like Dart Sass, True, SassDoc, and Herman: 22 | 23 | ``` 24 | yarn 25 | ``` 26 | 27 | The primary codebase is in the `sass/` folder, 28 | with matching tests in the `test/` directory. 29 | Any major code changes should also update the tests/docs. 30 | 31 | - `yarn test` will run the tests 32 | - `yarn docs` will build the documentation site 33 | - `yarn sass` will re-compile the code in `demo/` 34 | (mainly for experiments as you work) 35 | - `yarn commit` to run all three 36 | 37 | ## Conduct 38 | 39 | Please follow the 40 | [Sass Community Guidelines][guide] 41 | and [OddBird Code of Conduct][coc]. 42 | 43 | [guide]: https://sass-lang.com/community-guidelines 44 | [coc]: https://www.oddbird.net/conduct/ 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 OddBird (“Licensor”) 2 | 3 | Hippocratic License Version Number: 2.1. 4 | 5 | Purpose. The purpose of this License is for the Licensor named above to permit 6 | the Licensee (as defined below) broad permission, if consistent with Human 7 | Rights Laws and Human Rights Principles (as each is defined below), to use and 8 | work with the Software (as defined below) within the full scope of Licensor’s 9 | copyright and patent rights, if any, in the Software, while ensuring attribution 10 | and protecting the Licensor from liability. 11 | 12 | Permission and Conditions. The Licensor grants permission by this license 13 | (“License”), free of charge, to the extent of Licensor’s rights under applicable 14 | copyright and patent law, to any person or entity (the “Licensee”) obtaining a 15 | copy of this software and associated documentation files (the “Software”), to do 16 | everything with the Software that would otherwise infringe (i) the Licensor’s 17 | copyright in the Software or (ii) any patent claims to the Software that the 18 | Licensor can license or becomes able to license, subject to all of the following 19 | terms and conditions: 20 | 21 | - Acceptance. This License is automatically offered to every person and entity 22 | subject to its terms and conditions. Licensee accepts this License and agrees 23 | to its terms and conditions by taking any action with the Software that, 24 | absent this License, would infringe any intellectual property right held by 25 | Licensor. 26 | 27 | - Notice. Licensee must ensure that everyone who gets a copy of any part of this 28 | Software from Licensee, with or without changes, also receives the License and 29 | the above copyright notice (and if included by the Licensor, patent, trademark 30 | and attribution notice). Licensee must cause any modified versions of the 31 | Software to carry prominent notices stating that Licensee changed the 32 | Software. For clarity, although Licensee is free to create modifications of 33 | the Software and distribute only the modified portion created by Licensee with 34 | additional or different terms, the portion of the Software not modified must 35 | be distributed pursuant to this License. If anyone notifies Licensee in 36 | writing that Licensee has not complied with this Notice section, Licensee can 37 | keep this License by taking all practical steps to comply within 30 days after 38 | the notice. If Licensee does not do so, Licensee’s License (and all rights 39 | licensed hereunder) shall end immediately. 40 | 41 | - Compliance with Human Rights Principles and Human Rights Laws. 42 | 43 | 1. Human Rights Principles. 44 | 45 | (a) Licensee is advised to consult the articles of the United Nations 46 | Universal Declaration of Human Rights and the United Nations Global Compact 47 | that define recognized principles of international human rights (the “Human 48 | Rights Principles”). Licensee shall use the Software in a manner consistent 49 | with Human Rights Principles. 50 | 51 | (b) Unless the Licensor and Licensee agree otherwise, any dispute, 52 | controversy, or claim arising out of or relating to (i) Section 1(a) 53 | regarding Human Rights Principles, including the breach of Section 1(a), 54 | termination of this License for breach of the Human Rights Principles, or 55 | invalidity of Section 1(a) or (ii) a determination of whether any Law is 56 | consistent or in conflict with Human Rights Principles pursuant to Section 57 | 2, below, shall be settled by arbitration in accordance with the Hague 58 | Rules on Business and Human Rights Arbitration (the “Rules”); provided, 59 | however, that Licensee may elect not to participate in such arbitration, in 60 | which event this License (and all rights licensed hereunder) shall end 61 | immediately. The number of arbitrators shall be one unless the Rules 62 | require otherwise. 63 | 64 | Unless both the Licensor and Licensee agree to the contrary: (1) All 65 | documents and information concerning the arbitration shall be public and 66 | may be disclosed by any party; (2) The repository referred to under Article 67 | 43 of the Rules shall make available to the public in a timely manner all 68 | documents concerning the arbitration which are communicated to it, 69 | including all submissions of the parties, all evidence admitted into the 70 | record of the proceedings, all transcripts or other recordings of hearings 71 | and all orders, decisions and awards of the arbitral tribunal, subject only 72 | to the arbitral tribunal's powers to take such measures as may be necessary 73 | to safeguard the integrity of the arbitral process pursuant to Articles 18, 74 | 33, 41 and 42 of the Rules; and (3) Article 26(6) of the Rules shall not 75 | apply. 76 | 77 | 2. Human Rights Laws. The Software shall not be used by any person or entity 78 | for any systems, activities, or other uses that violate any Human Rights 79 | Laws. “Human Rights Laws” means any applicable laws, regulations, or rules 80 | (collectively, “Laws”) that protect human, civil, labor, privacy, 81 | political, environmental, security, economic, due process, or similar 82 | rights; provided, however, that such Laws are consistent and not in 83 | conflict with Human Rights Principles (a dispute over the consistency or a 84 | conflict between Laws and Human Rights Principles shall be determined by 85 | arbitration as stated above). Where the Human Rights Laws of more than one 86 | jurisdiction are applicable or in conflict with respect to the use of the 87 | Software, the Human Rights Laws that are most protective of the individuals 88 | or groups harmed shall apply. 89 | 90 | 3. Indemnity. Licensee shall hold harmless and indemnify Licensor (and any 91 | other contributor) against all losses, damages, liabilities, deficiencies, 92 | claims, actions, judgments, settlements, interest, awards, penalties, 93 | fines, costs, or expenses of whatever kind, including Licensor’s reasonable 94 | attorneys’ fees, arising out of or relating to Licensee’s use of the 95 | Software in violation of Human Rights Laws or Human Rights Principles. 96 | 97 | - Failure to Comply. Any failure of Licensee to act according to the terms and 98 | conditions of this License is both a breach of the License and an infringement 99 | of the intellectual property rights of the Licensor (subject to exceptions 100 | under Laws, e.g., fair use). In the event of a breach or infringement, the 101 | terms and conditions of this License may be enforced by Licensor under the 102 | Laws of any jurisdiction to which Licensee is subject. Licensee also agrees 103 | that the Licensor may enforce the terms and conditions of this License against 104 | Licensee through specific performance (or similar remedy under Laws) to the 105 | extent permitted by Laws. For clarity, except in the event of a breach of this 106 | License, infringement, or as otherwise stated in this License, Licensor may 107 | not terminate this License with Licensee. 108 | 109 | - Enforceability and Interpretation. If any term or provision of this License is 110 | determined to be invalid, illegal, or unenforceable by a court of competent 111 | jurisdiction, then such invalidity, illegality, or unenforceability shall not 112 | affect any other term or provision of this License or invalidate or render 113 | unenforceable such term or provision in any other jurisdiction; provided, 114 | however, subject to a court modification pursuant to the immediately following 115 | sentence, if any term or provision of this License pertaining to Human Rights 116 | Laws or Human Rights Principles is deemed invalid, illegal, or unenforceable 117 | against Licensee by a court of competent jurisdiction, all rights in the 118 | Software granted to Licensee shall be deemed null and void as between Licensor 119 | and Licensee. Upon a determination that any term or provision is invalid, 120 | illegal, or unenforceable, to the extent permitted by Laws, the court may 121 | modify this License to affect the original purpose that the Software be used 122 | in compliance with Human Rights Principles and Human Rights Laws as closely as 123 | possible. The language in this License shall be interpreted as to its fair 124 | meaning and not strictly for or against any party. 125 | 126 | - Disclaimer. TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE COMES “AS IS,” 127 | WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR AND ANY OTHER 128 | CONTRIBUTOR SHALL NOT BE LIABLE TO ANYONE FOR ANY DAMAGES OR OTHER LIABILITY 129 | ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THIS LICENSE, 130 | UNDER ANY KIND OF LEGAL CLAIM. 131 | 132 | This Hippocratic License is an Ethical Source license 133 | (https://ethicalsource.dev) and is offered for use by licensors and licensees at 134 | their own risk, on an “AS IS” basis, and with no warranties express or implied, 135 | to the maximum extent permitted by Laws. 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blend 2 | 3 | _CSS color spaces for [Dart Sass][]…_ 4 | 5 | [Dart Sass]: https://sass-lang.com/dart-sass 6 | 7 | CSS Color Module [Level 4][] & [Level 5][] 8 | include several new CSS color formats, 9 | new color-adjustment syntax, 10 | and a contrast function. 11 | **Blend** provides early access to many of these features, 12 | while working with Sass colors. 13 | 14 | [Level 4]: https://www.w3.org/TR/css-color-4/ 15 | [Level 5]: https://www.w3.org/TR/css-color-5/ 16 | 17 | Note that conversion between color-spaces 18 | requires gamut-adjustments and rounding. 19 | While we use the same conversion math recommended for browsers, 20 | pre-processing can result in slight variations in each step. 21 | Converting a color from one format to another 22 | and back again, may result in slight differences. 23 | 24 | **Our Top Priority** right now 25 | is full support for `color(display-p3 r g b / a)` 26 | which can already be used for wide-gamut colors 27 | in Safari. 28 | The conversion math is already supported, 29 | we just need to finalize the user-facing API. 30 | Help is welcome. 31 | 32 | ## Color Picker 33 | 34 | To get started with new CSS color formats 35 | (and LCH in particular), 36 | check out the wonderful 37 | [LCH Color Picker](https://css.land/lch/) 38 | by [Lea Verou](https://lea.verou.me/). 39 | 40 | We use the same conversion math, 41 | originally written in JS by 42 | [Chris Lilley](https://svgees.us/) 43 | and [Tab Atkins](https://www.xanthir.com/). 44 | 45 | ## Usage 46 | 47 | Download the files from GitHub, or install the npm package: 48 | 49 | ``` 50 | npm install @oddbird/blend [--save-dev] 51 | ``` 52 | 53 | Import with Dart Sass 54 | 55 | ```scss 56 | @use '/blend'; 57 | ``` 58 | 59 | ### Lab & LCH Formats 60 | 61 | (CIE) LCH & Lab color-conversion into (sRGB) sass colors: 62 | 63 | ```scss 64 | $cie-to-sass: ( 65 | blend.lch(30% 50 300), 66 | blend.lab(60% -60 60), 67 | 68 | blend.lch(60% 75 120, 50%), // as % 69 | blend.lab(60% -60 60, 0.5), // or as fraction 70 | ); 71 | ``` 72 | 73 | ### Color Contrast 74 | 75 | Based on the proposed Level 5 color-contrast() function: 76 | 77 | ```scss 78 | $contrast: ( 79 | // default black or white for best contrast 80 | blend.contrast($color), 81 | // highest contrast 82 | blend.contrast($color, maroon, rebeccapurple, cyan), 83 | // first color with contrast >= 4.5 84 | blend.contrast($color, maroon, rebeccapurple, 4.5), 85 | ); 86 | ``` 87 | 88 | ### Inspecting Colors 89 | 90 | Inspect LCH & Lab values of Sass colors: 91 | 92 | ```scss 93 | $inspect: ( 94 | blend.lightness($color), 95 | blend.a($color), 96 | blend.b($color), 97 | blend.chroma($color), 98 | blend.hue($color), 99 | ); 100 | ``` 101 | 102 | ### Relative Colors 103 | 104 | Relative Sass color adjustments using LCH & Lab channels 105 | 106 | ```scss 107 | $adjust: ( 108 | // set chroma to 10 109 | blend.set($color, $chroma: 10), 110 | // adjust hue by -10 111 | blend.adjust($color, $hue: -10), 112 | // scale lightness 10% lighter 113 | blend.scale($color, $lightness: 10%), 114 | ); 115 | ``` 116 | 117 | A relative-color shorthand, 118 | based on rough interpretation 119 | of the Level 5 relative color syntax: 120 | 121 | ```scss 122 | $from: ( 123 | // set chroma to 20 124 | blend.from($color, l, 20, h), 125 | // linear adjustments to a channel 126 | blend.from($color, l, c, h -60), 127 | // relative scale, e.g. "half-way to white" 128 | blend.from($color, l 50%, c, h), 129 | // multiply the channel value 130 | blend.from($color, 2l, c, h), 131 | ); 132 | ``` 133 | 134 | ## Todo 135 | 136 | The initial version is mostly focused on CIE colors, 137 | but Level 4 includes an array of new formats. 138 | We're working on it. 139 | 140 | See the full list of [planned enhancements][todo]. 141 | 142 | [todo]: https://github.com/oddbird/blend/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement 143 | 144 | ```scss 145 | @use 'blend'; 146 | 147 | $new-formats: ( 148 | blend.hwb(120deg 15% 15%), 149 | blend.color(display-p3 0.728 0.2824 0.4581), 150 | blend.color(rec-2020 0.6431 0.2955 0.4324), 151 | ..., 152 | ); 153 | 154 | $from-sass: ( 155 | blend.get($color, 'lch'), 156 | blend.get($color, 'lab'), 157 | blend.get($color, 'display-p3'), 158 | ..., 159 | ); 160 | 161 | $output: ( 162 | blend.string($color, 'lch'), 163 | blend.string($color, 'lab'), 164 | blend.string($color, 'display-p3'), 165 | ..., 166 | ); 167 | ``` 168 | 169 | --- 170 | 171 | ### Sponsor OddBird's OSS Work 172 | 173 | At OddBird, we love contributing to the languages & tools developers rely on. 174 | We're currently working on polyfills for new Popover & Anchor Positioning 175 | functionality, as well as CSS specifications for functions, mixins, and 176 | responsive typography. Help us keep this work sustainable and centered on your 177 | needs as a developer! We display sponsor logos and avatars on our 178 | [website](https://www.oddbird.net/blend/#open-source-sponsors). 179 | 180 | [Sponsor OddBird's OSS Work](https://opencollective.com/oddbird-open-source) 181 | -------------------------------------------------------------------------------- /_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'sass/blend'; 2 | -------------------------------------------------------------------------------- /demo/_gradient.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use 'sass:list'; 3 | @use 'sass:meta'; 4 | @use '../index' as blend; 5 | @use '../sass/utils/array'; 6 | 7 | $lch: 50% 50 _ !default; 8 | $ranges: (100 100 360); 9 | $index: list.index($lch, '_'); 10 | $range: list.nth($ranges, $index); 11 | 12 | // logic to create the color stops... 13 | @function _replace( 14 | $val, 15 | $stop 16 | ) { 17 | @return if($val == _, $stop * 2, $val); 18 | } 19 | 20 | @function _stops( 21 | $stop, 22 | $channels, 23 | $format 24 | ) { 25 | $channels: array.map($channels, meta.get-function('_replace'), $stop); 26 | @return if( 27 | ($format == 'hsl'), 28 | hsl($channels...), 29 | blend.lch($channels) 30 | ); 31 | } 32 | 33 | @function stops( 34 | $channels: $lch, 35 | $format: 'lch' 36 | ) { 37 | @return linear-gradient( 38 | to right, 39 | array.map( 40 | array.range(1, $range * 0.5, $separator: comma), 41 | meta.get-function('_stops'), 42 | $channels, 43 | $format 44 | ) 45 | ); 46 | } 47 | 48 | @function _hsl-hue-stops( 49 | $start 50 | ) { 51 | $start: hue 52 | } 53 | 54 | @function rgb-from-lch( 55 | $lch: $lch 56 | ) { 57 | $array: (); 58 | 59 | @if ($index != 3) { 60 | $mid: blend.lch(list.set-nth($lch, $index, $range * 0.5)); 61 | $lch: list.set-nth($lch, 3, color.hue($mid)); 62 | $array: array.range(1, ($range * 0.5), $separator: comma); 63 | } @else { 64 | $start: blend.lch(list.set-nth($lch, $index, 1)); 65 | $start-hue: color.hue($start); 66 | $array: array.range( 67 | $start-hue, 68 | ($start-hue + $range * 0.5), 69 | $separator: comma 70 | ); 71 | } 72 | 73 | $stops: array.map( 74 | $array, 75 | meta.get-function('_stops'), 76 | (list.nth($lch, 3) list.nth($lch, 2) * 1% list.nth($lch, 1)), 77 | 'hsl' 78 | ); 79 | 80 | @return linear-gradient( 81 | to right, 82 | $stops... 83 | ); 84 | } 85 | 86 | @function content( 87 | $lch: $lch 88 | ) { 89 | $lch: list.set-nth($lch, $index, '{1-#{$range}}'); 90 | @return 'lch(#{$lch})'; 91 | } 92 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Blend 7 | 8 | 9 | 10 |

Blending…

11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-image: linear-gradient(to right, rgb(193, 78, 118), rgb(194, 78, 116), rgb(194, 78, 113), rgb(194, 78, 110), rgb(194, 78, 107), rgb(194, 78, 104), rgb(194, 79, 101), rgb(194, 79, 98), rgb(194, 80, 96), rgb(194, 80, 93), rgb(193, 81, 90), rgb(193, 82, 87), rgb(192, 83, 85), rgb(191, 83, 82), rgb(191, 84, 79), rgb(190, 85, 77), rgb(189, 86, 74), rgb(188, 87, 72), rgb(187, 88, 69), rgb(185, 89, 67), rgb(184, 91, 64), rgb(183, 92, 62), rgb(181, 93, 59), rgb(180, 94, 57), rgb(178, 95, 55), rgb(177, 96, 52), rgb(175, 97, 50), rgb(173, 99, 48), rgb(172, 100, 46), rgb(170, 101, 44), rgb(168, 102, 42), rgb(166, 103, 40), rgb(164, 105, 38), rgb(162, 106, 36), rgb(160, 107, 34), rgb(158, 108, 33), rgb(155, 109, 31), rgb(153, 110, 29), rgb(151, 111, 28), rgb(148, 113, 27), rgb(146, 114, 26), rgb(143, 115, 25), rgb(141, 116, 24), rgb(138, 117, 23), rgb(136, 118, 22), rgb(133, 119, 22), rgb(131, 120, 22), rgb(128, 120, 22), rgb(125, 121, 22), rgb(122, 122, 23), rgb(119, 123, 23), rgb(117, 124, 24), rgb(114, 125, 25), rgb(111, 126, 27), rgb(108, 126, 28), rgb(105, 127, 30), rgb(102, 128, 31), rgb(98, 128, 33), rgb(95, 129, 35), rgb(92, 130, 37), rgb(89, 130, 39), rgb(85, 131, 41), rgb(82, 132, 43), rgb(78, 132, 45), rgb(74, 133, 48), rgb(70, 133, 50), rgb(66, 134, 52), rgb(62, 134, 55), rgb(58, 135, 57), rgb(53, 135, 60), rgb(48, 135, 62), rgb(42, 136, 65), rgb(35, 136, 68), rgb(28, 136, 70), rgb(18, 137, 73), rgb(2, 137, 76), rgb(0, 137, 80), rgb(0, 137, 84), rgb(0, 137, 88), rgb(0, 136, 91), rgb(0, 136, 94), rgb(0, 136, 97), rgb(0, 136, 100), rgb(0, 136, 103), rgb(0, 136, 106), rgb(0, 135, 108), rgb(0, 135, 111), rgb(0, 135, 113), rgb(0, 135, 115), rgb(0, 135, 118), rgb(0, 135, 120), rgb(0, 135, 122), rgb(0, 134, 124), rgb(0, 134, 126), rgb(0, 134, 128), rgb(0, 134, 130), rgb(0, 134, 131), rgb(0, 134, 133), rgb(0, 133, 135), rgb(0, 133, 137), rgb(0, 133, 138), rgb(0, 133, 140), rgb(0, 133, 142), rgb(0, 133, 144), rgb(0, 133, 145), rgb(0, 132, 147), rgb(0, 132, 149), rgb(0, 132, 150), rgb(0, 132, 152), rgb(0, 132, 154), rgb(0, 132, 156), rgb(0, 131, 157), rgb(0, 131, 159), rgb(0, 131, 161), rgb(0, 131, 163), rgb(0, 131, 165), rgb(0, 130, 167), rgb(0, 130, 169), rgb(0, 130, 171), rgb(0, 130, 173), rgb(0, 129, 175), rgb(0, 129, 177), rgb(0, 129, 180), rgb(0, 128, 182), rgb(0, 128, 185), rgb(0, 128, 187), rgb(0, 127, 190), rgb(0, 127, 193), rgb(0, 126, 196), rgb(0, 126, 200), rgb(0, 125, 203), rgb(21, 125, 204), rgb(35, 124, 204), rgb(45, 123, 204), rgb(54, 122, 205), rgb(61, 121, 205), rgb(68, 120, 204), rgb(74, 119, 204), rgb(80, 118, 204), rgb(86, 117, 203), rgb(91, 116, 203), rgb(96, 115, 202), rgb(101, 114, 201), rgb(105, 113, 201), rgb(110, 111, 200), rgb(114, 110, 199), rgb(118, 109, 197), rgb(122, 108, 196), rgb(126, 107, 195), rgb(130, 105, 193), rgb(133, 104, 192), rgb(137, 103, 190), rgb(140, 102, 189), rgb(144, 100, 187), rgb(147, 99, 185), rgb(150, 98, 183), rgb(153, 97, 181), rgb(156, 96, 179), rgb(159, 94, 177), rgb(161, 93, 175), rgb(164, 92, 172), rgb(166, 91, 170), rgb(169, 90, 168), rgb(171, 89, 165), rgb(173, 87, 163), rgb(175, 86, 160), rgb(177, 85, 158), rgb(179, 84, 155), rgb(181, 83, 152), rgb(182, 83, 150), rgb(184, 82, 147), rgb(185, 81, 144), rgb(186, 80, 141), rgb(188, 80, 138), rgb(189, 79, 136), rgb(190, 79, 133), rgb(191, 78, 130), rgb(192, 78, 127), rgb(192, 78, 124), rgb(193, 78, 121)), linear-gradient(to right, hsl(316.1739130435deg, 50%, 50%), hsl(318.1739130435deg, 50%, 50%), hsl(320.1739130435deg, 50%, 50%), hsl(322.1739130435deg, 50%, 50%), hsl(324.1739130435deg, 50%, 50%), hsl(326.1739130435deg, 50%, 50%), hsl(328.1739130435deg, 50%, 50%), hsl(330.1739130435deg, 50%, 50%), hsl(332.1739130435deg, 50%, 50%), hsl(334.1739130435deg, 50%, 50%), hsl(336.1739130435deg, 50%, 50%), hsl(338.1739130435deg, 50%, 50%), hsl(340.1739130435deg, 50%, 50%), hsl(342.1739130435deg, 50%, 50%), hsl(344.1739130435deg, 50%, 50%), hsl(346.1739130435deg, 50%, 50%), hsl(348.1739130435deg, 50%, 50%), hsl(350.1739130435deg, 50%, 50%), hsl(352.1739130435deg, 50%, 50%), hsl(354.1739130435deg, 50%, 50%), hsl(356.1739130435deg, 50%, 50%), hsl(358.1739130435deg, 50%, 50%), hsl(0.1739130435deg, 50%, 50%), hsl(2.1739130435deg, 50%, 50%), hsl(4.1739130435deg, 50%, 50%), hsl(6.1739130435deg, 50%, 50%), hsl(8.1739130435deg, 50%, 50%), hsl(10.1739130435deg, 50%, 50%), hsl(12.1739130435deg, 50%, 50%), hsl(14.1739130435deg, 50%, 50%), hsl(16.1739130435deg, 50%, 50%), hsl(18.1739130435deg, 50%, 50%), hsl(20.1739130435deg, 50%, 50%), hsl(22.1739130435deg, 50%, 50%), hsl(24.1739130435deg, 50%, 50%), hsl(26.1739130435deg, 50%, 50%), hsl(28.1739130435deg, 50%, 50%), hsl(30.1739130435deg, 50%, 50%), hsl(32.1739130435deg, 50%, 50%), hsl(34.1739130435deg, 50%, 50%), hsl(36.1739130435deg, 50%, 50%), hsl(38.1739130435deg, 50%, 50%), hsl(40.1739130435deg, 50%, 50%), hsl(42.1739130435deg, 50%, 50%), hsl(44.1739130435deg, 50%, 50%), hsl(46.1739130435deg, 50%, 50%), hsl(48.1739130435deg, 50%, 50%), hsl(50.1739130435deg, 50%, 50%), hsl(52.1739130435deg, 50%, 50%), hsl(54.1739130435deg, 50%, 50%), hsl(56.1739130435deg, 50%, 50%), hsl(58.1739130435deg, 50%, 50%), hsl(60.1739130435deg, 50%, 50%), hsl(62.1739130435deg, 50%, 50%), hsl(64.1739130435deg, 50%, 50%), hsl(66.1739130435deg, 50%, 50%), hsl(68.1739130435deg, 50%, 50%), hsl(70.1739130435deg, 50%, 50%), hsl(72.1739130435deg, 50%, 50%), hsl(74.1739130435deg, 50%, 50%), hsl(76.1739130435deg, 50%, 50%), hsl(78.1739130435deg, 50%, 50%), hsl(80.1739130435deg, 50%, 50%), hsl(82.1739130435deg, 50%, 50%), hsl(84.1739130435deg, 50%, 50%), hsl(86.1739130435deg, 50%, 50%), hsl(88.1739130435deg, 50%, 50%), hsl(90.1739130435deg, 50%, 50%), hsl(92.1739130435deg, 50%, 50%), hsl(94.1739130435deg, 50%, 50%), hsl(96.1739130435deg, 50%, 50%), hsl(98.1739130435deg, 50%, 50%), hsl(100.1739130435deg, 50%, 50%), hsl(102.1739130435deg, 50%, 50%), hsl(104.1739130435deg, 50%, 50%), hsl(106.1739130435deg, 50%, 50%), hsl(108.1739130435deg, 50%, 50%), hsl(110.1739130435deg, 50%, 50%), hsl(112.1739130435deg, 50%, 50%), hsl(114.1739130435deg, 50%, 50%), hsl(116.1739130435deg, 50%, 50%), hsl(118.1739130435deg, 50%, 50%), hsl(120.1739130435deg, 50%, 50%), hsl(122.1739130435deg, 50%, 50%), hsl(124.1739130435deg, 50%, 50%), hsl(126.1739130435deg, 50%, 50%), hsl(128.1739130435deg, 50%, 50%), hsl(130.1739130435deg, 50%, 50%), hsl(132.1739130435deg, 50%, 50%), hsl(134.1739130435deg, 50%, 50%), hsl(136.1739130435deg, 50%, 50%), hsl(138.1739130435deg, 50%, 50%), hsl(140.1739130435deg, 50%, 50%), hsl(142.1739130435deg, 50%, 50%), hsl(144.1739130435deg, 50%, 50%), hsl(146.1739130435deg, 50%, 50%), hsl(148.1739130435deg, 50%, 50%), hsl(150.1739130435deg, 50%, 50%), hsl(152.1739130435deg, 50%, 50%), hsl(154.1739130435deg, 50%, 50%), hsl(156.1739130435deg, 50%, 50%), hsl(158.1739130435deg, 50%, 50%), hsl(160.1739130435deg, 50%, 50%), hsl(162.1739130435deg, 50%, 50%), hsl(164.1739130435deg, 50%, 50%), hsl(166.1739130435deg, 50%, 50%), hsl(168.1739130435deg, 50%, 50%), hsl(170.1739130435deg, 50%, 50%), hsl(172.1739130435deg, 50%, 50%), hsl(174.1739130435deg, 50%, 50%), hsl(176.1739130435deg, 50%, 50%), hsl(178.1739130435deg, 50%, 50%), hsl(180.1739130435deg, 50%, 50%), hsl(182.1739130435deg, 50%, 50%), hsl(184.1739130435deg, 50%, 50%), hsl(186.1739130435deg, 50%, 50%), hsl(188.1739130435deg, 50%, 50%), hsl(190.1739130435deg, 50%, 50%), hsl(192.1739130435deg, 50%, 50%), hsl(194.1739130435deg, 50%, 50%), hsl(196.1739130435deg, 50%, 50%), hsl(198.1739130435deg, 50%, 50%), hsl(200.1739130435deg, 50%, 50%), hsl(202.1739130435deg, 50%, 50%), hsl(204.1739130435deg, 50%, 50%), hsl(206.1739130435deg, 50%, 50%), hsl(208.1739130435deg, 50%, 50%), hsl(210.1739130435deg, 50%, 50%), hsl(212.1739130435deg, 50%, 50%), hsl(214.1739130435deg, 50%, 50%), hsl(216.1739130435deg, 50%, 50%), hsl(218.1739130435deg, 50%, 50%), hsl(220.1739130435deg, 50%, 50%), hsl(222.1739130435deg, 50%, 50%), hsl(224.1739130435deg, 50%, 50%), hsl(226.1739130435deg, 50%, 50%), hsl(228.1739130435deg, 50%, 50%), hsl(230.1739130435deg, 50%, 50%), hsl(232.1739130435deg, 50%, 50%), hsl(234.1739130435deg, 50%, 50%), hsl(236.1739130435deg, 50%, 50%), hsl(238.1739130435deg, 50%, 50%), hsl(240.1739130435deg, 50%, 50%), hsl(242.1739130435deg, 50%, 50%), hsl(244.1739130435deg, 50%, 50%), hsl(246.1739130435deg, 50%, 50%), hsl(248.1739130435deg, 50%, 50%), hsl(250.1739130435deg, 50%, 50%), hsl(252.1739130435deg, 50%, 50%), hsl(254.1739130435deg, 50%, 50%), hsl(256.1739130435deg, 50%, 50%), hsl(258.1739130435deg, 50%, 50%), hsl(260.1739130435deg, 50%, 50%), hsl(262.1739130435deg, 50%, 50%), hsl(264.1739130435deg, 50%, 50%), hsl(266.1739130435deg, 50%, 50%), hsl(268.1739130435deg, 50%, 50%), hsl(270.1739130435deg, 50%, 50%), hsl(272.1739130435deg, 50%, 50%), hsl(274.1739130435deg, 50%, 50%), hsl(276.1739130435deg, 50%, 50%), hsl(278.1739130435deg, 50%, 50%), hsl(280.1739130435deg, 50%, 50%), hsl(282.1739130435deg, 50%, 50%), hsl(284.1739130435deg, 50%, 50%), hsl(286.1739130435deg, 50%, 50%), hsl(288.1739130435deg, 50%, 50%), hsl(290.1739130435deg, 50%, 50%), hsl(292.1739130435deg, 50%, 50%), hsl(294.1739130435deg, 50%, 50%), hsl(296.1739130435deg, 50%, 50%), hsl(298.1739130435deg, 50%, 50%), hsl(300.1739130435deg, 50%, 50%), hsl(302.1739130435deg, 50%, 50%), hsl(304.1739130435deg, 50%, 50%), hsl(306.1739130435deg, 50%, 50%), hsl(308.1739130435deg, 50%, 50%), hsl(310.1739130435deg, 50%, 50%), hsl(312.1739130435deg, 50%, 50%), hsl(314.1739130435deg, 50%, 50%), hsl(316.1739130435deg, 50%, 50%)); 3 | background-size: auto 50vh, cover; 4 | background-repeat: no-repeat; 5 | display: grid; 6 | place-content: center; 7 | margin: 0; 8 | min-height: 100vh; 9 | } 10 | 11 | h1 { 12 | background: rgba(46, 19, 28, 0.8); 13 | color: rgb(255, 216, 227); 14 | border-radius: 0.2em; 15 | font-family: "Open Sans", "Helvetica Neue", sans-serif; 16 | padding: 0.5em; 17 | } 18 | h1::after { 19 | display: inline-block; 20 | content: "lch(50% 50 {1-360})"; 21 | font-family: "Courier New", Courier, monospace; 22 | margin-inline-start: 0.4em; 23 | } 24 | 25 | /*# sourceMappingURL=style.css.map */ 26 | -------------------------------------------------------------------------------- /demo/style.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAYA;EACE,kBACE;EAGF;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA","file":"style.css"} -------------------------------------------------------------------------------- /demo/style.scss: -------------------------------------------------------------------------------- 1 | @use '../index' as blend; 2 | @use '../sass/utils/array'; 3 | @use 'sass:list'; 4 | @use 'sass:map'; 5 | @use 'sass:meta'; 6 | 7 | // we'll create gradients on the _ channels 8 | @use 'gradient' with ( 9 | $lch: 50% 50 _, 10 | ); 11 | 12 | // layout 13 | body { 14 | background-image: 15 | gradient.stops(), 16 | gradient.rgb-from-lch() 17 | ; 18 | background-size: auto 50vh, cover; 19 | background-repeat: no-repeat; 20 | display: grid; 21 | place-content: center; 22 | margin: 0; 23 | min-height: 100vh; 24 | } 25 | 26 | h1 { 27 | background: blend.lch(10% 15 0, 80%); 28 | color: blend.lch(90% 40 0); 29 | border-radius: 0.2em; 30 | font-family: 'Open Sans', 'Helvetica Neue', sans-serif; 31 | padding: 0.5em; 32 | 33 | &::after { 34 | display: inline-block; 35 | content: gradient.content(); 36 | font-family: 'Courier New', Courier, monospace; 37 | margin-inline-start: 0.4em; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oddbird/blend", 3 | "version": "0.2.4", 4 | "title": "Blend", 5 | "description": "New color formats, adjustments, and conversions in Sass", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "main": "index.scss", 10 | "author": "Miriam Suzanne", 11 | "license": "SEE LICENSE IN LICENSE.md", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/oddbird/blend.git" 15 | }, 16 | "bugs": "https://github.com/oddbird/blend/issues", 17 | "homepage": "https://www.oddbird.net/blend", 18 | "keywords": [ 19 | "sass", 20 | "scss", 21 | "css", 22 | "colors", 23 | "design", 24 | "design systems", 25 | "CIE", 26 | "Lab", 27 | "sRGB", 28 | "hwb", 29 | "LCH" 30 | ], 31 | "scripts": { 32 | "json": "sass src/json.scss src/json.css", 33 | "herman": "sassdoc sass/", 34 | "docs": "yarn json; yarn herman", 35 | "sass": "sass demo/style.scss demo/style.css", 36 | "test": "mocha", 37 | "commit": "yarn test; yarn docs; yarn sass" 38 | }, 39 | "devDependencies": { 40 | "mocha": "^10.2.0", 41 | "sass": "^1.56.2", 42 | "sass-true": "^7.0.0", 43 | "sassdoc": "^2.7.4", 44 | "sassdoc-theme-herman": "^5.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sass/_blend.scss: -------------------------------------------------------------------------------- 1 | @forward 'cie'; 2 | @forward 'inspect'; 3 | @forward 'adjust'; 4 | -------------------------------------------------------------------------------- /sass/adjust/_channels.scss: -------------------------------------------------------------------------------- 1 | @use '../utils/throw'; 2 | @use 'parse'; 3 | @use 'sass:map'; 4 | @use 'sass:math'; 5 | @use 'sass:meta'; 6 | 7 | /// # Relative Colors 8 | /// The primary advantage of LCH color 9 | /// is the ability to 10 | /// generate one color from another 11 | /// by adjust channels, 12 | /// and expect consistent results. 13 | /// 14 | /// @link https://www.w3.org/TR/css-color-5/ CSS Colors Level 5 15 | /// @group adjust 16 | 17 | /// Set individual Lab or LCH channels 18 | /// to specific values, 19 | /// and return the adjusted color. 20 | /// All arguments must be given by keyword, 21 | /// rather than positional order. 22 | /// @example scss 23 | /// .set { 24 | /// chroma: blend.set(rebeccapurple, $chroma: 20); 25 | /// shorthand: blend.set(rebeccapurple, $c: 20); 26 | /// } 27 | /// @param {color} $color - 28 | /// The initial Sass color being adjusted 29 | /// @param {percentage} $lightness [null] - 30 | /// Set lightness to a percentage between 0 and 100% 31 | /// (Can also use the `$l` keyword shorthand) 32 | /// @param {number} $chroma [null] - 33 | /// Set chroma to a positive number (generally 0-100) 34 | /// (Can also use the `$c` keyword shorthand) 35 | /// @param {angle} $hue [null] - 36 | /// Set hue to an angle (0deg-360deg) 37 | /// (Can also use the `$h` keyword shorthand) 38 | /// @param {number} $a [null] - 39 | /// Set the Lab `a` channel to any number ±160 40 | /// @param {number} $b [null] - 41 | /// Set the Lab `b` channel to any number ±160 42 | /// @throw Cannot set both Lab & LCH channels at once 43 | /// @group adjust 44 | @function set( 45 | $color, 46 | $channels... 47 | ) { 48 | $adjust: parse.args(meta.keywords($channels), 'set'); 49 | @return parse.do('set', $color, $adjust); 50 | } 51 | 52 | /// Adjust individual Lab or LCH channels 53 | /// up or down by a given amount, 54 | /// and return the adjusted color. 55 | /// All arguments must be given by keyword, 56 | /// rather than positional order. 57 | /// @example scss 58 | /// .set { 59 | /// hue: blend.adjust(rebeccapurple, $hue: -60); 60 | /// shorthand: blend.adjust(rebeccapurple, $h: -60); 61 | /// } 62 | /// @param {color} $color - 63 | /// The initial Sass color being adjusted 64 | /// @param {number} $lightness [null] - 65 | /// Adjust lightness by given amount 66 | /// (Can also use the `$l` keyword shorthand) 67 | /// @param {number} $chroma [null] - 68 | /// Adjust chroma by given amount 69 | /// (Can also use the `$c` keyword shorthand) 70 | /// @param {number} $hue [null] - 71 | /// Adjust hue by given number of degrees 72 | /// (Can also use the `$h` keyword shorthand) 73 | /// @param {number} $a [null] - 74 | /// Adjust Lab `a` by given amount 75 | /// @param {number} $b [null] - 76 | /// Adjust Lab `b` by given amount 77 | /// @throw Cannot adjust both Lab & LCH channels at once 78 | /// @group adjust 79 | @function adjust( 80 | $color, 81 | $channels... 82 | ) { 83 | $adjust: parse.args(meta.keywords($channels), 'adjust'); 84 | @return parse.do('adjust', $color, $adjust); 85 | } 86 | 87 | /// Fluidly scale Lab or LCH channels 88 | /// up or down by a given percentage, 89 | /// and return the adjusted color. 90 | /// All arguments must be given by keyword, 91 | /// rather than positional order. 92 | /// @example scss 93 | /// .set { 94 | /// lightness: blend.adjust(rebeccapurple, $lightness: 50%); 95 | /// shorthand: blend.adjust(rebeccapurple, $l: 50%); 96 | /// } 97 | /// @param {color} $color - 98 | /// The initial Sass color being adjusted 99 | /// @param {percentage} $lightness [null] - 100 | /// Scale lightness by given percentage 101 | /// of the distance towards 0 or 100 102 | /// (Can also use the `$l` keyword shorthand) 103 | /// @param {percentage} $chroma [null] - 104 | /// Scale chroma by given percentage 105 | /// of the distance towards 0 or 100 106 | /// (Can also use the `$c` keyword shorthand) 107 | /// @param {percentage} $hue [null] - 108 | /// Adjust hue by given percentage of the hue wheel 109 | /// (Can also use the `$h` keyword shorthand) 110 | /// @param {percentage} $a [null] - 111 | /// Scale Lab `a` by given percentage 112 | /// of the distance towards 160 or -160 113 | /// @param {percentage} $b [null] - 114 | /// Scale Lab `b` by given percentage 115 | /// of the distance towards 160 or -160 116 | /// @throw Cannot scale both Lab & LCH channels at once 117 | /// @throw Scales must be defined using `%` units 118 | /// @group adjust 119 | @function scale( 120 | $color, 121 | $channels... 122 | ) { 123 | $channels: meta.keywords($channels); 124 | 125 | @each $channel in map.values($channels) { 126 | @if (math.unit($channel) != '%') { 127 | @return throw.error( 128 | 'Scales must be defined using `%` units', 129 | 'scale()' 130 | ); 131 | } 132 | } 133 | 134 | $adjust: parse.args($channels, 'scale'); 135 | @return parse.do('scale', $color, $adjust); 136 | } 137 | -------------------------------------------------------------------------------- /sass/adjust/_from.scss: -------------------------------------------------------------------------------- 1 | @use '../utils/array'; 2 | @use '../convert'; 3 | @use '../cie'; 4 | @use 'parse'; 5 | @use 'sass:color'; 6 | @use 'sass:list'; 7 | 8 | /// We also provide a compact syntax for LCH adjustments, 9 | /// based on the proposed CSS _relative color syntax_. 10 | /// Since Sass is not able to mimic the syntax exactly, 11 | /// we've developed a shorthand based on the CSS proposal. 12 | /// 13 | /// For each channel: 14 | /// - The single-letter channel name (`l | c | h`) 15 | /// represents no change to the current value of the channel 16 | /// - A single number will replace the current channel value 17 | /// - A number with `l | c | h` units will multiply the channel 18 | /// - `l | c | h` followed by a percentage will scale the channel 19 | /// relative to available range 20 | /// - `l | c | h` followed by an amount 21 | /// will add or subtract that amount from the current value 22 | /// 23 | /// @link https://www.w3.org/TR/css-color-5/#relative-colors CSS Specification 24 | /// @colors adjust-from 25 | /// @example scss 26 | /// .from { 27 | /// // set chroma to 20 28 | /// set: blend.from(rebeccapurple, l, 20, h); 29 | /// 30 | /// // linear adjustments to a channel 31 | /// adjust: blend.from(rebeccapurple, l, c, h -60); 32 | /// 33 | /// // relative scale, e.g. "half-way to white" 34 | /// scale: blend.from(rebeccapurple, l 50%, c, h); 35 | /// 36 | /// // multiply the channel value 37 | /// multiply: blend.from(rebeccapurple, 2l, c, h); 38 | /// } 39 | /// @param {color} $color - 40 | /// The initial Sass color being adjusted 41 | /// @param {l | number | l number} $l [l] - 42 | /// Adjustments to the `l` channel: 43 | /// @param {c | number | c number} $c [c] - 44 | /// Adjustments to the `c` channel 45 | /// @param {h | number | h number} $h [h] - 46 | /// Adjustments to the `h` channel 47 | /// @return {color} - The final color after adjustments 48 | /// @group adjust 49 | @function from( 50 | $color, 51 | $lightness: l, 52 | $chroma: c, 53 | $hue: h, 54 | ) { 55 | $a: color.alpha($color); 56 | $lch: convert.sassToLCH($color); 57 | 58 | @for $i from 1 through 3 { 59 | $val: list.nth($lch, $i); 60 | $adjust: list.nth($lightness $chroma $hue, $i); 61 | $key: list.nth(l c h, $i); 62 | $lch: list.set-nth($lch, $i, parse.from($val, $adjust, $key)); 63 | } 64 | 65 | @return cie.lch($lch, $a); 66 | } 67 | -------------------------------------------------------------------------------- /sass/adjust/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'channels'; 2 | @forward 'from'; 3 | -------------------------------------------------------------------------------- /sass/adjust/_parse.scss: -------------------------------------------------------------------------------- 1 | @use '../cie'; 2 | @use '../convert'; 3 | @use '../utils/array'; 4 | @use '../utils/units'; 5 | @use '../utils/throw'; 6 | @use 'utils'; 7 | @use 'sass:color'; 8 | @use 'sass:list'; 9 | @use 'sass:map'; 10 | @use 'sass:math'; 11 | @use 'sass:meta'; 12 | 13 | $utils: meta.module-functions('utils'); 14 | 15 | @function from( 16 | $val, 17 | $adjust, 18 | $key, 19 | ) { 20 | // l -> l 21 | @if ($adjust == $key) { 22 | @return $val; 23 | } 24 | 25 | @if (meta.type-of($adjust) == 'number') { 26 | // 2l -> 2*l 27 | @if (math.unit($adjust) == $key) { 28 | @return $val * units.strip($adjust); 29 | } 30 | 31 | // 20 -> 20 32 | @return $adjust; 33 | } 34 | 35 | @if (meta.type-of($adjust) == 'function') { 36 | @return meta.call($adjust, $val); 37 | } 38 | 39 | @if (meta.type-of($adjust) == 'list') and (list.nth($adjust, 1) == $key) { 40 | $adjust: list.nth($adjust, 2); 41 | 42 | @if (meta.type-of($adjust) == 'number') { 43 | // scale 44 | @if (math.unit($adjust) == '%') { 45 | @return utils.scale($val, $adjust, if(($key == 'h'), 360, 0 100)); 46 | } 47 | 48 | // linear adjust 49 | @return $val + $adjust; 50 | } 51 | } 52 | 53 | @return throw.error( 54 | '`#{$adjust}` is not a known adjustment for `#{$key}` channel', 55 | 'from()' 56 | ); 57 | } 58 | 59 | @function args( 60 | $args, 61 | $source, 62 | ) { 63 | $l: map.get($args, lightness) or map.get($args, l); 64 | $c: map.get($args, chroma) or map.get($args, c); 65 | $h: map.get($args, hue) or map.get($args, h); 66 | $a: map.get($args, a); 67 | $b: map.get($args, b); 68 | 69 | @if ($c or $h) { 70 | @if ($a or $b) { 71 | @return throw.error( 72 | 'Cannot #{$source} both Lab ($a, $b) & LCH ($c, $h) channels at once', 73 | '#{$source}()' 74 | ); 75 | } 76 | 77 | @return ( 78 | l: $l, 79 | c: $c, 80 | h: if($h and $source != 'scale', units.to-degrees($h), $h), 81 | ); 82 | } 83 | 84 | @return ( 85 | l: $l, 86 | a: $a, 87 | b: $b, 88 | ); 89 | } 90 | 91 | @function do( 92 | $function, 93 | $color, 94 | $adjust, 95 | ) { 96 | $fn: null; 97 | $keys: map.keys($adjust); 98 | $format: array.join($keys); 99 | 100 | $a: color.alpha($color); 101 | $color: if( 102 | ($format == 'lab'), 103 | convert.sassToLab($color), 104 | convert.sassToLCH($color), 105 | ); 106 | 107 | @each $channel, $amount in $adjust { 108 | @if ($amount) { 109 | $fn: $fn or map.get($utils, $function); 110 | $i: list.index($keys, $channel); 111 | $val: nth($color, $i); 112 | $do: ($val, $amount); 113 | 114 | // range arg for scale… 115 | @if ($function == 'scale') { 116 | $ranges: ( 117 | h: 360, 118 | a: -160 160, 119 | b: -160 160, 120 | ); 121 | $do: list.append($do, map.get($ranges, $channel) or (0 100)); 122 | } 123 | 124 | $val: meta.call($fn, $do...); 125 | $color: list.set-nth($color, $i, $val); 126 | } 127 | } 128 | 129 | @return if( 130 | ($format == 'lab'), 131 | cie.lab($color, $a), 132 | cie.lch($color, $a), 133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /sass/adjust/_utils.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @use 'sass:meta'; 3 | 4 | @function set( 5 | $val, 6 | $to, 7 | ) { 8 | @return $to; 9 | } 10 | 11 | @function adjust( 12 | $val, 13 | $amount, 14 | ) { 15 | @return $val + $amount; 16 | } 17 | 18 | @function scale( 19 | $val, 20 | $amount, 21 | $range: 0 100 22 | ) { 23 | $amount: math.div($amount, 100%); 24 | 25 | @if (type-of($range) == 'number') { 26 | @return ($val + ($amount * $range)) % $range; 27 | } 28 | 29 | $dif: if( 30 | $amount < 0, 31 | $val - math.min($range...), 32 | math.max($range...) - $val 33 | ); 34 | 35 | @return $val + ($dif * $amount); 36 | } 37 | -------------------------------------------------------------------------------- /sass/cie/_api.scss: -------------------------------------------------------------------------------- 1 | @use '../convert'; 2 | @use 'lch'; 3 | 4 | /// # Lab & LCH Formats 5 | /// Lab and LCH provide a perceptually uniform color space. 6 | /// These functions are based on the CSS specification, 7 | /// but eagerly converted to Sass sRGB. 8 | /// 9 | /// While we are using the same math recommended for browsers, 10 | /// this sort of color-space conversion involves 11 | /// gamut-adjustments and rounding. 12 | /// In a pre-processor, 13 | /// that math is compiled once, 14 | /// and the original data is not maintained. 15 | /// A color converted into Sass (sRGB) format 16 | /// and then back to CIE 17 | /// may be slightly different from the original. 18 | /// 19 | /// We use chroma-reduction to achieve 20 | /// a relative-colormetric closest match 21 | /// for CIE colors that fall outside the sRGB gamut. 22 | /// 23 | /// @link https://www.w3.org/TR/css-color-4/#lab-colors CSS specification 24 | /// @group cie-formats 25 | 26 | 27 | /// Define Sass colors in `lch()` syntax. 28 | /// 29 | /// @link https://www.w3.org/TR/css-color-4/#funcdef-lch CSS lch() specification 30 | /// @colors cie-lch 31 | /// @example scss 32 | /// .lch { 33 | /// // rebeccapurple 34 | /// lch: blend.lch(32.39% 61.25 308.86deg); 35 | /// 36 | /// // 60% opacity 37 | /// lch-a: blend.lch(32.39% 61.25 308.86deg, 60%); 38 | /// } 39 | /// @param {list} $lch - 40 | /// A space-separated list of 41 | /// _lightness_, _chroma_, and _hue_ channel values. 42 | /// - **lightness** is a percentage (0%-100%) 43 | /// - **chroma** is a positive number (generally 0-100) 44 | /// - **hue** is an angle (0deg-360deg) 45 | /// @param {number} $a [100%] - 46 | /// Alpha opacity, 47 | /// as either a unitless fraction (0-1) 48 | /// or a percentage (0%-100%) 49 | /// @return {color} - 50 | /// Converted sRGB value 51 | /// @group cie-formats 52 | @function lch( 53 | $lch, 54 | $a: 100%, 55 | ) { 56 | $lch: lch.normalize($lch); 57 | @return convert.rgbToSass(lch.gamut-correct($lch...), $a); 58 | } 59 | 60 | 61 | /// Define Sass colors in `lab()` syntax. 62 | /// 63 | /// @colors cie-lab 64 | /// @example scss 65 | /// .lab { 66 | /// // rebeccapurple 67 | /// lab: blend.lab(32.39% 38.43 -47.69); 68 | /// 69 | /// // 60% alpha opacity 70 | /// lab-a: blend.lab(32.39% 38.43 -47.69, 0.6); 71 | /// } 72 | /// 73 | /// @link https://www.w3.org/TR/css-color-4/#funcdef-lab CSS lab() specification 74 | /// @param {list} $lab - 75 | /// A space-separated list of 76 | /// _lightness_, _a_, and _b_ channel values 77 | /// - **lightness** is a percentage (0%-100%) 78 | /// - **a** & **b** are both numbers (±160) 79 | /// @param {number} $a [100%] - 80 | /// Alpha opacity, 81 | /// as either a unitless fraction (0-1) 82 | /// or a percentage (0%-100%) 83 | /// @group cie-formats 84 | @function lab( 85 | $lab, 86 | $a: 100%, 87 | ) { 88 | @return lch(convert.Lab_to_LCH($lab), $a); 89 | } 90 | -------------------------------------------------------------------------------- /sass/cie/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'api'; 2 | -------------------------------------------------------------------------------- /sass/cie/_lch.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:list'; 2 | @use 'sass:math'; 3 | @use 'sass:meta'; 4 | @use '../utils/array'; 5 | @use '../utils/units'; 6 | @use '../convert'; 7 | 8 | @function normalize( 9 | $lch, 10 | ) { 11 | $lch: list.set-nth($lch, 3, units.to-degrees(list.nth($lch, 3))); 12 | @return units.strip-all($lch); 13 | } 14 | 15 | @function _channel-in_srgb( 16 | $is, 17 | $val, 18 | $range: 0 1, 19 | ) { 20 | $E: 0.000005; 21 | @return $is and ($val == math.clamp(0 - $E, $val, 1 + $E)); 22 | } 23 | 24 | @function in-srgb( 25 | $rgb 26 | ) { 27 | @return array.reduce( 28 | $rgb, 29 | meta.get-function('_channel-in_srgb'), 30 | true 31 | ); 32 | } 33 | 34 | @function to-srgb( 35 | $lch, 36 | $check-gamut: false 37 | ) { 38 | $result: convert.LCH_to_sRGB($lch); 39 | 40 | @if ($check-gamut) { 41 | @return if(in-srgb($result), $result, null); 42 | } 43 | 44 | @return $result; 45 | } 46 | 47 | // gamut correction 48 | @function gamut-correct( 49 | $l, $c, $h 50 | ) { 51 | // Moves an lch color into the sRGB gamut 52 | // by holding the l and h steady, 53 | // and adjusting the c via binary-search 54 | // until the color is on the sRGB boundary. 55 | $output: to-srgb($l $c $h, true); 56 | 57 | @if (not $output) { 58 | $hiC: $c; 59 | $loC: 0; 60 | 61 | $c: $c * 0.5; 62 | 63 | // 0.0001 chosen fairly arbitrarily as "close enough" 64 | @while (($hiC - $loC) > 0.0001) { 65 | @if to-srgb($l $c $h, true) { 66 | $loC: $c; 67 | } @else { 68 | $hiC: $c; 69 | } 70 | $c: ($hiC + $loC) * 0.5; 71 | } 72 | 73 | $output: to-srgb($l $c $h); 74 | } 75 | 76 | @return $output; 77 | } 78 | -------------------------------------------------------------------------------- /sass/convert/_conversions.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:list'; 2 | @use 'sass:math'; 3 | @use 'sass:meta'; 4 | @use '../utils/array'; 5 | @use '../utils/matrix'; 6 | @use '../utils/pow'; 7 | 8 | // ********************* 9 | // Based on: https://drafts.csswg.org/css-color-4/conversions.js 10 | // Credit: Chris Lilley, https://svgees.us/ 11 | // ********************* 12 | 13 | $Im: math.div(216, 24389); // 6^3/29^3 14 | $Io: math.div(24389, 27); // 29^3/3^3 15 | $Ip: 1.09929682680944 ; 16 | $I2: 0.018053968510807; 17 | $white: [0.96422, 1.00000, 0.82521]; // D50 reference white 18 | 19 | // Sample code for color conversions 20 | // Conversion can also be done using ICC profiles and a Color Management System 21 | // For clarity, a library is used for matrix manipulations 22 | 23 | // sRGB-related functions 24 | 25 | @function _lin_sRGB($val) { 26 | @if ($val < 0.04045) { 27 | @return math.div($val, 12.92); 28 | } 29 | 30 | @return math.pow(math.div($val + 0.055, 1.055), 2.4); 31 | } 32 | 33 | // rgb [0, 1] <--> 34 | @function lin_sRGB($RGB) { 35 | // convert an array of sRGB values in the range 0.0 - 1.0 36 | // to linear light (un-companded) form. 37 | // https://en.wikipedia.org/wiki/SRGB 38 | @return array.map($RGB, meta.get-function('_lin_sRGB')); 39 | } 40 | 41 | @function _gam_sRGB($val) { 42 | @if ($val > 0.0031308) { 43 | @return 1.055 * math.pow($val, math.div(1, 2.4)) - 0.055; 44 | } 45 | 46 | @return 12.92 * $val; 47 | } 48 | 49 | // rgb [0, 1] <--> 50 | @function gam_sRGB($RGB) { 51 | // convert an array of linear-light sRGB values in the range 0.0-1.0 52 | // to gamma corrected form 53 | // https://en.wikipedia.org/wiki/SRGB 54 | @return array.map($RGB, meta.get-function('_gam_sRGB')); 55 | } 56 | 57 | // rgb [0, 1] -> xyz 58 | @function lin_sRGB_to_XYZ($rgb) { 59 | // convert an array of linear-light sRGB values to CIE XYZ 60 | // using sRGB's own white, D65 (no chromatic adaptation) 61 | // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html 62 | // also 63 | // https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz 64 | $M: [ 65 | [ 0.41239079926595934, 0.357584339383878, 0.1804807884018343 ], 66 | [ 0.21263900587151027, 0.715168678767756, 0.07219231536073371 ], 67 | [ 0.01933081871559182, 0.11919477979462598, 0.9505321522496607 ], 68 | ]; 69 | 70 | @return matrix.multiply($M, $rgb); 71 | } 72 | 73 | // xyz -> rgb [0, 1] 74 | @function XYZ_to_lin_sRGB($XYZ) { 75 | // convert XYZ to linear-light sRGB 76 | $M: [ 77 | [ 3.2409699419045226, -1.537383177570094, -0.4986107602930034 ], 78 | [ -0.9692436362808796, 1.8759675015077202, 0.04155505740717559 ], 79 | [ 0.05563007969699366, -0.20397695888897652, 1.0569715142428786 ], 80 | ]; 81 | 82 | @return matrix.multiply($M, $XYZ); 83 | } 84 | 85 | // display-p3-related functions 86 | 87 | // p3 [0, 1] <--> 88 | @function lin_P3($RGB) { 89 | // convert an array of display-p3 RGB values in the range 0.0 - 1.0 90 | // to linear light (un-companded) form. 91 | 92 | @return lin_sRGB($RGB); // same as sRGB 93 | } 94 | 95 | // p3 [0, 1] <--> 96 | @function gam_P3($RGB) { 97 | // convert an array of linear-light display-p3 RGB in the range 0.0-1.0 98 | // to gamma corrected form 99 | 100 | @return gam_sRGB($RGB); // same as sRGB 101 | } 102 | 103 | // p3 [0, 1] -> xyz 104 | @function lin_P3_to_XYZ($rgb) { 105 | // convert an array of linear-light display-p3 values to CIE XYZ 106 | // using D65 (no chromatic adaptation) 107 | // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html 108 | $M: [ 109 | [0.4865709486482162, 0.26566769316909306, 0.1982172852343625], 110 | [0.2289745640697488, 0.6917385218365064, 0.079286914093745], 111 | [0.0000000000000000, 0.04511338185890264, 1.043944368900976], 112 | ]; 113 | // 0 was computed as -3.972075516933488e-17 114 | 115 | @return matrix.multiply($M, $rgb); 116 | } 117 | 118 | // xyz -> p3 [0, 1] 119 | @function XYZ_to_lin_P3($XYZ) { 120 | // convert XYZ to linear-light P3 121 | $M: [ 122 | [ 2.493496911941425, -0.9313836179191239, -0.40271078445071684], 123 | [-0.8294889695615747, 1.7626640603183463, 0.023624685841943577], 124 | [ 0.03584583024378447, -0.07617238926804182, 0.9568845240076872], 125 | ]; 126 | 127 | @return matrix.multiply($M, $XYZ); 128 | } 129 | 130 | // prophoto-rgb functions 131 | 132 | @function _lin_ProPhoto($val) { 133 | @if ($val < 0.031248) { 134 | @return math.div($val, 16); 135 | } 136 | 137 | @return math.pow($val, 1.8); 138 | } 139 | 140 | // prophoto [0, 1] <--> 141 | @function lin_ProPhoto($RGB) { 142 | // convert an array of prophoto-rgb values in the range 0.0 - 1.0 143 | // to linear light (un-companded) form. 144 | // Transfer curve is gamma 1.8 with a small linear portion 145 | @return array.map($RGB, meta.get-function('_lin_ProPhoto')); 146 | } 147 | 148 | @function _gam_ProPhoto($val) { 149 | @if ($val > 0.001953) { 150 | @return math.pow($val, math.div(1, 1.8)); 151 | } 152 | 153 | @return 16 * $val; 154 | } 155 | 156 | // prophoto [0, 1] <--> 157 | @function gam_ProPhoto($RGB) { 158 | // convert an array of linear-light prophoto-rgb in the range 0.0-1.0 159 | // to gamma corrected form 160 | // Transfer curve is gamma 1.8 with a small linear portion 161 | @return array.map($RGB, meta.get-function('_gam_ProPhoto')); 162 | } 163 | 164 | // prophoto [0, 1] -> xyz 165 | @function lin_ProPhoto_to_XYZ($rgb) { 166 | // convert an array of linear-light prophoto-rgb values to CIE XYZ 167 | // using D50 (so no chromatic adaptation needed afterwards) 168 | // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html 169 | $M: [ 170 | [ 0.7977604896723027, 0.13518583717574031, 0.0313493495815248 ], 171 | [ 0.2880711282292934, 0.7118432178101014, 0.00008565396060525902 ], 172 | [ 0.0, 0.0, 0.8251046025104601 ], 173 | ]; 174 | 175 | @return matrix.multiply($M, $rgb); 176 | } 177 | 178 | // xyz -> prophoto [0, 1] 179 | @function XYZ_to_lin_ProPhoto($XYZ) { 180 | // convert XYZ to linear-light prophoto-rgb 181 | $M: [ 182 | [ 1.3457989731028281, -0.25558010007997534, -0.05110628506753401 ], 183 | [ -0.5446224939028347, 1.5082327413132781, 0.02053603239147973 ], 184 | [ 0.0, 0.0, 1.2119675456389454 ], 185 | ]; 186 | 187 | @return matrix.multiply($M, $XYZ); 188 | } 189 | 190 | // a98-rgb functions 191 | 192 | @function _lin_a98rgb($val) { 193 | @return math.pow($val, math.div(563, 256)); 194 | } 195 | 196 | // a98 [0, 1] <--> 197 | @function lin_a98rgb($RGB) { 198 | // convert an array of a98-rgb values in the range 0.0 - 1.0 199 | // to linear light (un-companded) form. 200 | @return array.map($RGB, meta.get-function('_lin_a98rgb')); 201 | } 202 | 203 | @function _gam_a98rgb($val) { 204 | @return math.pow($val, math.div(256, 563)); 205 | } 206 | 207 | // a98 [0, 1] <--> 208 | @function gam_a98rgb($RGB) { 209 | // convert an array of linear-light a98-rgb in the range 0.0-1.0 210 | // to gamma corrected form 211 | @return array.map($RGB, meta.get-function('_gam_a98rgb')); 212 | } 213 | 214 | // a98 [0, 1] -> xyz 215 | @function lin_a98rgb_to_XYZ($rgb) { 216 | // convert an array of linear-light a98-rgb values to CIE XYZ 217 | // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html 218 | // has greater numerical precision than section 4.3.5.3 of 219 | // https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf 220 | // but the values below were calculated from first principles 221 | // from the chromaticity coordinates of R G B W 222 | // see matrixmaker.html 223 | $M: [ 224 | [ 0.5766690429101305, 0.1855582379065463, 0.1882286462349947 ], 225 | [ 0.29734497525053605, 0.6273635662554661, 0.07529145849399788 ], 226 | [ 0.02703136138641234, 0.07068885253582723, 0.9913375368376388 ], 227 | ]; 228 | 229 | @return matrix.multiply($M, $rgb); 230 | } 231 | 232 | // xyz -> a98 [0, 1] 233 | @function XYZ_to_lin_a98rgb($XYZ) { 234 | // convert XYZ to linear-light a98-rgb 235 | $M: [ 236 | [ 2.0415879038107465, -0.5650069742788596, -0.34473135077832956 ], 237 | [ -0.9692436362808795, 1.8759675015077202, 0.04155505740717557 ], 238 | [ 0.013444280632031142, -0.11836239223101838, 1.0151749943912054 ], 239 | ]; 240 | 241 | @return matrix.multiply($M, $XYZ); 242 | } 243 | 244 | // Rec. 2020-related functions 245 | 246 | @function _lin_2020($val) { 247 | @if ($val < $I2 * 4.5 ) { 248 | @return math.div($val, 4.5); 249 | } 250 | 251 | @return math.pow(math.div($val + $Ip - 1, $Ip), 2.4); 252 | } 253 | 254 | // r2020 [0, 1] <--> 255 | @function lin_2020($RGB) { 256 | // convert an array of rec2020 RGB values in the range 0.0 - 1.0 257 | // to linear light (un-companded) form. 258 | @return array.map($RGB, meta.get-function('_lin_2020')); 259 | } 260 | // check with standard this really is 2.4 and 1/2.4, not 0.45 was wikipedia claims 261 | 262 | @function _gam_2020($val) { 263 | @if ($val > $I2) { 264 | @return $Ip * math.pow($val, math.div(1, 2.4)) - ($Ip - 1); 265 | } 266 | 267 | @return 4.5 * $val; 268 | } 269 | 270 | // r2020 [0, 1] <--> 271 | @function gam_2020($RGB) { 272 | // convert an array of linear-light rec2020 RGB in the range 0.0-1.0 273 | // to gamma corrected form 274 | @return array.map($RGB, meta.get-function('_gam_2020')); 275 | } 276 | 277 | // r2020 [0, 1] -> xyz 278 | @function lin_2020_to_XYZ($rgb) { 279 | // convert an array of linear-light rec2020 values to CIE XYZ 280 | // using D65 (no chromatic adaptation) 281 | // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html 282 | $M: [ 283 | [0.6369580483012914, 0.14461690358620832, 0.1688809751641721], 284 | [0.2627002120112671, 0.6779980715188708, 0.05930171646986196], 285 | [0.000000000000000, 0.028072693049087428, 1.060985057710791], 286 | ]; 287 | // 0 is actually calculated as 4.994106574466076e-17 288 | 289 | @return matrix.multiply($M, $rgb); 290 | } 291 | 292 | // xyz -> r2020 [0, 1] 293 | @function XYZ_to_lin_2020($XYZ) { 294 | // convert XYZ to linear-light rec2020 295 | $M: [ 296 | [1.7166511879712674, -0.35567078377639233, -0.25336628137365974], 297 | [-0.6666843518324892, 1.6164812366349395, 0.01576854581391113], 298 | [0.017639857445310783, -0.042770613257808524, 0.9421031212354738], 299 | ]; 300 | 301 | @return matrix.multiply($M, $XYZ); 302 | } 303 | 304 | // Chromatic adaptation 305 | 306 | // xyz <--> 307 | @function D65_to_D50($XYZ) { 308 | // Bradford chromatic adaptation from D65 to D50 309 | // The matrix below is the result of three operations: 310 | // - convert from XYZ to retinal cone domain 311 | // - scale components from one reference white to another 312 | // - convert back to XYZ 313 | // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html 314 | $M: [ 315 | [ 1.0478112, 0.0228866, -0.0501270], 316 | [ 0.0295424, 0.9904844, -0.0170491], 317 | [-0.0092345, 0.0150436, 0.7521316] 318 | ]; 319 | 320 | @return matrix.multiply($M, $XYZ); 321 | } 322 | 323 | // xyz <--> 324 | @function D50_to_D65($XYZ) { 325 | // Bradford chromatic adaptation from D50 to D65 326 | $M: [ 327 | [ 0.9555766, -0.0230393, 0.0631636], 328 | [-0.0282895, 1.0099416, 0.0210077], 329 | [ 0.0122982, -0.0204830, 1.3299098], 330 | ]; 331 | 332 | @return matrix.multiply($M, $XYZ); 333 | } 334 | 335 | // Lab and LCH 336 | 337 | @function _XYZ_to_Lab-XYZ( 338 | $value, 339 | $i 340 | ) { 341 | @return math.div($value, list.nth($white, $i)); 342 | } 343 | 344 | @function _XYZ_to_Lab-f( 345 | $value 346 | ) { 347 | @if ($value > $Im) { 348 | @return pow.cbrt($value); 349 | } 350 | 351 | @return math.div($Io * $value + 16, 116); 352 | } 353 | 354 | // xyz -> l [0+] ab [±160] 355 | @function XYZ_to_Lab($XYZ) { 356 | // Assuming XYZ is relative to D50, convert to CIE Lab 357 | // from CIE standard, which now defines these as a rational fraction 358 | 359 | // compute xyz, which is XYZ scaled relative to reference white 360 | $xyz: array.map($XYZ, get-function('_XYZ_to_Lab-XYZ'), i); 361 | 362 | // now compute f 363 | $f: array.map($xyz, get-function('_XYZ_to_Lab-f')); 364 | 365 | @return [ 366 | (116 * list.nth($f, 2)) - 16, 367 | 500 * (list.nth($f, 1) - list.nth($f, 2)), 368 | 200 * (list.nth($f, 2) - list.nth($f, 3)), 369 | ]; 370 | } 371 | 372 | @function _Lab_to_XYZ( 373 | $value, 374 | $i 375 | ) { 376 | @return $value * list.nth($white, $i); 377 | } 378 | 379 | // l [0+] ab [±160] -> xyz 380 | @function Lab_to_XYZ($Lab) { 381 | // Convert Lab to D50-adapted XYZ 382 | // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html 383 | $f: array.template($Lab); 384 | 385 | $Lab0: list.nth($Lab, 1); 386 | $Lab1: list.nth($Lab, 2); 387 | $Lab2: list.nth($Lab, 3); 388 | 389 | // compute f, starting with the luminance-related term 390 | $f1: math.div($Lab0 + 16, 116); 391 | $f0: math.div($Lab1, 500) + $f1; 392 | $f2: $f1 - math.div($Lab2, 200); 393 | 394 | // compute xyz 395 | $xyz: [ 396 | if(math.pow($f0, 3) > $Im, math.pow($f0, 3), math.div(116 * $f0 - 16, $Io)), 397 | if($Lab0 > $Io * $Im, math.pow(math.div($Lab0 + 16, 116), 3), math.div($Lab0, $Io)), 398 | if(math.pow($f2, 3) > $Im, math.pow($f2, 3), math.div(116 * $f2 - 16, $Io)), 399 | ]; 400 | 401 | // Compute XYZ by scaling xyz by reference white 402 | @return array.map($xyz, meta.get-function('_Lab_to_XYZ'), i); 403 | } 404 | 405 | // l [0+] ab [±160] -> lc [0+] h [0, 360) 406 | @function Lab_to_LCH($Lab) { 407 | // Convert to polar form 408 | $Lab0: list.nth($Lab, 1); 409 | $Lab1: list.nth($Lab, 2); 410 | $Lab2: list.nth($Lab, 3); 411 | 412 | // L is still L 413 | // Chroma 414 | // Hue, in degrees [0 to 360) 415 | @return [ 416 | $Lab0, 417 | math.sqrt(math.pow($Lab1, 2) + math.pow($Lab2, 2)), 418 | math.div(math.atan2($Lab2, $Lab1), 1deg) % 360, 419 | ]; 420 | } 421 | 422 | // lc [0+] h [0, 360) -> l [0+] ab [±160] 423 | @function LCH_to_Lab($LCH) { 424 | // Convert from polar form 425 | $LCH0: list.nth($LCH, 1); 426 | $LCH1: list.nth($LCH, 2); 427 | $LCH2: list.nth($LCH, 3); 428 | 429 | @return [ 430 | $LCH0, 431 | $LCH1 * math.cos(math.div($LCH2 * math.$pi, 180)), 432 | $LCH1 * math.sin(math.div($LCH2 * math.$pi, 180)), 433 | ]; 434 | } 435 | -------------------------------------------------------------------------------- /sass/convert/_extra.scss: -------------------------------------------------------------------------------- 1 | @use '../utils/array'; 2 | @use '../utils/channel'; 3 | @use '../utils/units'; 4 | @use 'utilities' as *; 5 | @use 'conversions' as *; 6 | @use 'sass:color'; 7 | @use 'sass:list'; 8 | @use 'sass:math'; 9 | 10 | @function hueFromColor( 11 | $color 12 | ) { 13 | @return channel.deg6(units.strip(color.hue($color))); 14 | } 15 | 16 | // rgb [0, 1] -> sass color 17 | @function rgbToSass( 18 | $rgb, 19 | $a: 100% 20 | ) { 21 | $rgb: channel.format($rgb); 22 | $rgba: list.append($rgb, $a); 23 | @return rgba($rgba...); 24 | } 25 | 26 | // sass color -> rgb [0, 1] 27 | @function sassToRgb( 28 | $color 29 | ) { 30 | $rgb: ( 31 | color.red($color), 32 | color.green($color), 33 | color.blue($color), 34 | ); 35 | @return channel.fractions(units.strip-all($rgb), 255); 36 | } 37 | 38 | // rgb [0, 1] -> l [0+] ab [±160] 39 | @function sRGB_to_Lab($RGB) { 40 | @return XYZ_to_Lab(D65_to_D50(lin_sRGB_to_XYZ(lin_sRGB($RGB)))); 41 | } 42 | 43 | // sass color -> h [0, 6) sl [0, 1] 44 | @function sassToHsl( 45 | $color 46 | ) { 47 | @return ( 48 | hueFromColor($color), 49 | channel.fraction(units.strip(color.saturation($color))), 50 | channel.fraction(units.strip(color.lightness($color))), 51 | ); 52 | } 53 | 54 | // rgb [0, 1] -> h [0, 6) sl [0, 1] 55 | @function rgbToHsl( 56 | $rgb 57 | ) { 58 | $temp: array.template($rgb); 59 | $hsl: sassToHsl(rgbToSass($rgb)); 60 | @return list.join($temp, $hsl); 61 | } 62 | 63 | // rgb [0, 1] -> h [0, 6) wb [0, 1] 64 | @function rgbToHwb( 65 | $rgb 66 | ) { 67 | $temp: array.template($rgb); 68 | $hwb: ( 69 | hueFromColor(rgbToSass($rgb)), 70 | math.min($rgb...), 71 | 1 - math.max($rgb...), 72 | ); 73 | @return list.join($temp, $hwb); 74 | } 75 | 76 | // h [0, 6) wb [0, 1] -> rgb [0, 1] 77 | @function hwbToRgb($hwb) { 78 | $hue: list.nth($hwb, 1); 79 | $white: list.nth($hwb, 2); 80 | $black: list.nth($hwb, 3); 81 | $rgb: hslToRgb($hue 1 0.5); 82 | @for $i from 1 through 3 { 83 | $x: list.nth($rgb, $i); 84 | $x: $x * (1 - $white - $black) + $white; 85 | $rgb: list.set-nth($rgb, $i, $x); 86 | } 87 | @return $rgb; 88 | } 89 | 90 | @function sassToLCH( 91 | $color 92 | ) { 93 | @return sRGB_to_LCH(sassToRgb($color)); 94 | } 95 | 96 | @function sassToLab( 97 | $color 98 | ) { 99 | @return sRGB_to_Lab(sassToRgb($color)); 100 | } 101 | -------------------------------------------------------------------------------- /sass/convert/_index.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:list'; 2 | @forward 'conversions'; 3 | @forward 'utilities'; 4 | @forward 'extra'; 5 | 6 | $srgb: ('rgb', 'hsl', 'hwb', 'device-cmyk', 'srgb', auto); 7 | 8 | @function to( 9 | $format 10 | ) { 11 | @return if( 12 | list.index($srgb, $format), 13 | 'rgb', 14 | $format 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /sass/convert/_utilities.scss: -------------------------------------------------------------------------------- 1 | @use '../utils/array'; 2 | @use 'conversions' as *; 3 | @use 'sass:list'; 4 | @use 'sass:math'; 5 | 6 | // ********************* 7 | // Based on: https://drafts.csswg.org/css-color-4/utilities.js 8 | // Credit: Chris Lilley, https://svgees.us/ 9 | // ********************* 10 | 11 | // rgb [0, 1] 12 | @function sRGB_to_luminance($RGB) { 13 | // convert an array of gamma-corrected sRGB values 14 | // in the 0.0 to 1.0 range 15 | // to linear-light sRGB, then to CIE XYZ 16 | // and return luminance (the Y value) 17 | 18 | $XYZ: lin_sRGB_to_XYZ(lin_sRGB($RGB)); 19 | @return list.nth($XYZ, 2); 20 | } 21 | 22 | // lc [0+] h [0, 360) 23 | @function LCH_to_luminance($LCH) { 24 | $XYZ: Lab_to_XYZ(LCH_to_Lab($LCH)); 25 | @return list.nth($XYZ, 2); 26 | } 27 | 28 | @function contrast($RGB1, $RGB2) { 29 | // return WCAG 2.1 contrast ratio 30 | // https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio 31 | // for two sRGB values 32 | // given as arrays of 0.0 to 1.0 33 | 34 | $L1: sRGB_to_luminance($RGB1) + 0.05; 35 | $L2: sRGB_to_luminance($RGB2) + 0.05; 36 | 37 | @return math.div(math.max($L1, $L2), math.min($L2, $L1)); 38 | } 39 | 40 | // rgb [0, 1] -> lc [0+] h [0, 360) 41 | @function sRGB_to_LCH($RGB) { 42 | // convert an array of gamma-corrected sRGB values 43 | // in the 0.0 to 1.0 range 44 | // to linear-light sRGB, then to CIE XYZ, 45 | // then adapt from D65 to D50, 46 | // then convert XYZ to CIE Lab 47 | // and finally, convert to CIE LCH 48 | 49 | @return Lab_to_LCH(XYZ_to_Lab(D65_to_D50(lin_sRGB_to_XYZ(lin_sRGB($RGB))))); 50 | } 51 | 52 | // rgb [0, 1] -> lc [0+] h [0, 360) 53 | @function P3_to_LCH($RGB) { 54 | // convert an array of gamma-corrected display-p3 values 55 | // in the 0.0 to 1.0 range 56 | // to linear-light display-p3, then to CIE XYZ, 57 | // then adapt from D65 to D50, 58 | // then convert XYZ to CIE Lab 59 | // and finally, convert to CIE LCH 60 | 61 | @return Lab_to_LCH(XYZ_to_Lab(D65_to_D50(lin_P3_to_XYZ(lin_P3($RGB))))); 62 | } 63 | 64 | // rgb [0, 1] -> lc [0+] h [0, 360) 65 | @function r2020_to_LCH($RGB) { 66 | // convert an array of gamma-corrected rec.2020 values 67 | // in the 0.0 to 1.0 range 68 | // to linear-light sRGB, then to CIE XYZ, 69 | // then adapt from D65 to D50, 70 | // then convert XYZ to CIE Lab 71 | // and finally, convert to CIE LCH 72 | 73 | @return Lab_to_LCH(XYZ_to_Lab(D65_to_D50(lin_2020_to_XYZ(lin_2020($RGB))))); 74 | } 75 | 76 | // l [0+] ab [±160] -> rgb [0, 1] 77 | @function Lab_to_sRGB($Lab) { 78 | // convert an array of CIE Lab values to XYZ, 79 | // adapt from D50 to D65, 80 | // then convert XYZ to linear-light sRGB 81 | // and finally to gamma corrected sRGB 82 | // for in-gamut colors, components are in the 0.0 to 1.0 range 83 | // out of gamut colors may have negative components 84 | // or components greater than 1.0 85 | // so check for that :) 86 | 87 | @return gam_sRGB(XYZ_to_lin_sRGB(D50_to_D65(Lab_to_XYZ($Lab)))); 88 | } 89 | 90 | // lc [0+] h [0, 360) -> rgb [0, 1] 91 | @function LCH_to_sRGB($LCH) { 92 | // convert an array of CIE LCH values 93 | // to CIE Lab, and then see Lab_to_sRGB… 94 | @return Lab_to_sRGB(LCH_to_Lab($LCH)); 95 | } 96 | 97 | // lc [0+] h [0, 360) -> rgb [0, 1] 98 | @function LCH_to_P3($LCH) { 99 | // convert an array of CIE LCH values 100 | // to CIE Lab, and then to XYZ, 101 | // adapt from D50 to D65, 102 | // then convert XYZ to linear-light display-p3 103 | // and finally to gamma corrected display-p3 104 | // for in-gamut colors, components are in the 0.0 to 1.0 range 105 | // out of gamut colors may have negative components 106 | // or components greater than 1.0 107 | // so check for that :) 108 | 109 | @return gam_P3(XYZ_to_lin_P3(D50_to_D65(Lab_to_XYZ(LCH_to_Lab($LCH))))); 110 | } 111 | 112 | // lc [0+] h [0, 360) -> rgb [0, 1] 113 | @function LCH_to_r2020($LCH) { 114 | // convert an array of CIE LCH values 115 | // to CIE Lab, and then to XYZ, 116 | // adapt from D50 to D65, 117 | // then convert XYZ to linear-light rec.2020 118 | // and finally to gamma corrected rec.2020 119 | // for in-gamut colors, components are in the 0.0 to 1.0 range 120 | // out of gamut colors may have negative components 121 | // or components greater than 1.0 122 | // so check for that :) 123 | 124 | @return gam_2020(XYZ_to_lin_2020(D50_to_D65(Lab_to_XYZ(LCH_to_Lab($LCH))))); 125 | } 126 | 127 | // this is straight from the CSS Color 4 spec 128 | 129 | // h [0, 6) sl [0, 1] -> rgb [0, 1] 130 | @function hslToRgb($hsl) { 131 | // For simplicity, this algorithm assumes that the hue has been normalized 132 | // to a number in the half-open range [0, 6), and the saturation and lightness 133 | // have been normalized to the range [0, 1]. It returns an array of three numbers 134 | // representing the red, green, and blue channels of the colors, 135 | // normalized to the range [0, 1] 136 | $temp: array.template($hsl); 137 | $hue: list.nth($hsl, 1); 138 | $sat: list.nth($hsl, 2); 139 | $light: list.nth($hsl, 3); 140 | 141 | $t2: null; 142 | 143 | @if ( $light <= .5 ) { 144 | $t2: $light * ($sat + 1); 145 | } @else { 146 | $t2: $light + $sat - ($light * $sat); 147 | } 148 | 149 | $t1: $light * 2 - $t2; 150 | $r: hueToRgb($t1, $t2, $hue + 2); 151 | $g: hueToRgb($t1, $t2, $hue); 152 | $b: hueToRgb($t1, $t2, $hue - 2); 153 | @return list.join($temp, $r $g $b); 154 | } 155 | 156 | @function hueToRgb($t1, $t2, $hue) { 157 | $hue: if(($hue < 0), $hue + 6, $hue); 158 | $hue: if(($hue >= 6), $hue - 6, $hue); 159 | 160 | @if ($hue < 1) { 161 | @return ($t2 - $t1) * $hue + $t1; 162 | } @else if ($hue < 3) { 163 | @return $t2; 164 | } @else if ($hue < 4) { 165 | @return ($t2 - $t1) * (4 - $hue) + $t1; 166 | } 167 | 168 | @return $t1; 169 | } 170 | 171 | // These are the naive algorithms from CS Color 4 172 | 173 | // cmyk [0, 1] -> rgb [0, 1] 174 | @function naive_CMYK_to_sRGB($CMYK) { 175 | // CMYK is an array of four values 176 | // in the range [0.0, 1.0] 177 | // the output is an array of [RGB] 178 | // also in the [0.0, 1.0] range 179 | // because the naive algorithm does not generate out of gamut colors 180 | // neither does it generate accurate simulations of practical CMYK colors 181 | 182 | $cyan: list.nth($CMYK, 1); 183 | $magenta: list.nth($CMYK, 2); 184 | $yellow: list.nth($CMYK, 3); 185 | $black: list.nth($CMYK, 4); 186 | 187 | $red: 1 - math.min(1, $cyan * (1 - $black) + $black); 188 | $green: 1 - math.min(1, $magenta * (1 - $black) + $black); 189 | $blue: 1 - math.min(1, $yellow * (1 - $black) + $black); 190 | 191 | @return [$red, $green, $blue]; 192 | } 193 | 194 | // rgb [0, 1] -> cmyk [0, 1] 195 | @function naive_sRGB_to_CMYK($RGB) { 196 | $temp: array.template($RGB); 197 | // RGB is an array of three values 198 | // in the range [0.0, 1.0] 199 | // the output is an array of [CMYK] 200 | // also in the [0.0, 1.0] range 201 | // with maximum GCR and (I think) 200% TAC 202 | // the naive algorithm does not generate out of gamut colors 203 | // neither does it generate accurate simulations of practical CMYK colors 204 | 205 | $red: list.nth($RGB, 1); 206 | $green: list.nth($RGB, 2); 207 | $blue: list.nth($RGB, 3); 208 | 209 | $black: 1 - math.max($red, $green, $blue); 210 | $cyan: if(($black == 1.0), 0, math.div(1 - $red - $black, 1 - $black)); 211 | $magenta: if(($black == 1.0), 0, math.div(1 - $green - $black, 1 - $black)); 212 | $yellow: if(($black == 1.0), 0, math.div(1 - $blue - $black, 1 - $black)); 213 | 214 | @return list.join($temp, $cyan $magenta $yellow $black); 215 | } 216 | -------------------------------------------------------------------------------- /sass/inspect/_cie.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:list'; 2 | @use 'sass:math'; 3 | @use '../convert'; 4 | 5 | 6 | /// # Inspecting Colors 7 | /// Similar to Sass built-in tools for inspecting 8 | /// the rgb or hsl values of a color, 9 | /// these functions return LCH and Lab channel values. 10 | /// 11 | /// For these demos, we'll inspect the following colors: 12 | /// @colors inspect-cie 13 | /// @group cie-inspect 14 | 15 | 16 | /// Lab/LCH lightness is not the same as HSL/HWB lightness, 17 | /// so this value will not match the output 18 | /// of Sass built-in `color.lightness()` function. 19 | /// 20 | /// @example scss 21 | /// .lightness { 22 | /// papayawhip: blend.lightness(papayawhip); 23 | /// rebeccapurple: blend.lightness(rebeccapurple); 24 | /// yellow: blend.lightness(yellow); 25 | /// deeppink: blend.lightness(deeppink); 26 | /// } 27 | /// 28 | /// @param {color} $color - The Sass color to inspect 29 | /// @return {number} - The Lab/LCH "lightness" channel of the color 30 | /// @group cie-inspect 31 | @function lightness( 32 | $color, 33 | ) { 34 | @return list.nth(convert.sassToLab($color), 1) * 1%; 35 | } 36 | 37 | /// @example scss 38 | /// .a { 39 | /// papayawhip: blend.a(papayawhip); 40 | /// rebeccapurple: blend.a(rebeccapurple); 41 | /// yellow: blend.a(yellow); 42 | /// deeppink: blend.a(deeppink); 43 | /// } 44 | /// 45 | /// @param {color} $color - The Sass color to inspect 46 | /// @return {number} - The Lab "a" channel of the color 47 | /// @group cie-inspect 48 | @function a( 49 | $color 50 | ) { 51 | @return list.nth(convert.sassToLab($color), 2); 52 | } 53 | 54 | /// @example scss 55 | /// .b { 56 | /// papayawhip: blend.b(papayawhip); 57 | /// rebeccapurple: blend.b(rebeccapurple); 58 | /// yellow: blend.b(yellow); 59 | /// deeppink: blend.b(deeppink); 60 | /// } 61 | /// 62 | /// @param {color} $color - The Sass color to inspect 63 | /// @return {number} - The Lab "b" channel of the color 64 | /// @group cie-inspect 65 | @function b( 66 | $color 67 | ) { 68 | @return list.nth(convert.sassToLab($color), 3); 69 | } 70 | 71 | /// @example scss 72 | /// .chroma { 73 | /// papayawhip: blend.chroma(papayawhip); 74 | /// rebeccapurple: blend.chroma(rebeccapurple); 75 | /// yellow: blend.chroma(yellow); 76 | /// deeppink: blend.chroma(deeppink); 77 | /// } 78 | /// 79 | /// @param {color} $color - The Sass color to inspect 80 | /// @return {number} - The LCH "chroma" channel of the color 81 | /// @group cie-inspect 82 | @function chroma( 83 | $color 84 | ) { 85 | @return list.nth(convert.sassToLCH($color), 2); 86 | } 87 | 88 | /// Lab/LCH hue is not the same as HSL/HWB hue, 89 | /// so this value will not match the output 90 | /// of Sass built-in `color.hue()` function. 91 | /// 92 | /// @example scss 93 | /// .hue { 94 | /// papayawhip: blend.hue(papayawhip); 95 | /// rebeccapurple: blend.hue(rebeccapurple); 96 | /// yellow: blend.hue(yellow); 97 | /// deeppink: blend.hue(deeppink); 98 | /// } 99 | /// 100 | /// @param {color} $color - The Sass color to inspect 101 | /// @return {number} - The LCH "hue" channel of the color 102 | /// @group cie-inspect 103 | @function hue( 104 | $color 105 | ) { 106 | @return list.nth(convert.sassToLCH($color), 3) * 1deg; 107 | } 108 | -------------------------------------------------------------------------------- /sass/inspect/_contrast.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:list'; 2 | @use 'sass:meta'; 3 | @use '../utils/array'; 4 | @use '../utils/throw'; 5 | @use '../convert'; 6 | 7 | /// # Color Contrast 8 | /// Proper contrast is important for design legibility & accessibility. 9 | /// 10 | /// @link https://www.w3.org/TR/css-color-5/ CSS specification 11 | /// @group contrast 12 | 13 | /// CSS Color Module, level 5, 14 | /// defines a `color-contrast()` function 15 | /// that can be used to select the best contrast 16 | /// from a list of colors. 17 | /// 18 | /// @link https://www.w3.org/TR/css-color-5/#colorcontrast CSS color-contrast() 19 | /// @colors inspect-contrast 20 | /// @example scss 21 | /// .contrast { 22 | /// // black or white for best contrast 23 | /// default: blend.contrast(papayawhip); 24 | /// 25 | /// // highest contrast 26 | /// highest: blend.contrast(papayawhip, rebeccapurple, maroon); 27 | /// 28 | /// // first option with contrast >= 4.5 29 | /// first: blend.contrast(papayawhip, rebeccapurple, maroon, 4.5); 30 | /// } 31 | /// @param {color} $color - 32 | /// Base color to contrast against 33 | /// @param $options... [(black, white)] - 34 | /// Returns the better contrast of black or white by default. 35 | /// - Optionally provide two or more colors, 36 | /// to compare for highest contrast with the base color. 37 | /// - Optionally provide a contrast ratio as the final value, 38 | /// to return the first color option that passes 39 | /// the given contrast threshold 40 | /// @return {color} - 41 | /// The option with the highest contrast, orr first to meet a given ratio 42 | /// @group contrast 43 | @function contrast( 44 | $color, 45 | $options... 46 | ) { 47 | $color: convert.sassToRgb($color); 48 | $length: list.length($options); 49 | $ratio: null; 50 | 51 | @if ($length == 0) { 52 | $options: black white; 53 | } @else if ($length == 1) { 54 | @return throw.error( 55 | 'Provide at least two color options to select from', 56 | 'contrast()' 57 | ) 58 | } @else if ($length > 1) { 59 | $last: list.nth($options, -1); 60 | 61 | @if (meta.type-of($last) == 'number') { 62 | $ratio: $last; 63 | $options: array.slice($options, 0, -1); 64 | } 65 | } 66 | 67 | $best-color: null; 68 | $best-ratio: 0; 69 | 70 | @each $option in $options { 71 | $try: convert.sassToRgb($option); 72 | $contrast: convert.contrast($color, $try); 73 | 74 | @if $ratio and ($contrast >= $ratio) { 75 | @return $option; 76 | } 77 | 78 | @if ($contrast > $best-ratio) { 79 | $best-color: $option; 80 | $best-ratio: $contrast; 81 | } 82 | } 83 | 84 | @return $best-color; 85 | } 86 | -------------------------------------------------------------------------------- /sass/inspect/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'contrast'; 2 | @forward 'cie'; 3 | -------------------------------------------------------------------------------- /sass/utils/_array.scss: -------------------------------------------------------------------------------- 1 | @use 'throw'; 2 | @use 'sass:list'; 3 | @use 'sass:math'; 4 | @use 'sass:meta'; 5 | @use 'sass:string'; 6 | 7 | @function template( 8 | $array, 9 | $separator: null, 10 | $brackets: null 11 | ) { 12 | $error: throw.type($array, 'list' 'arglist', 'array.template()', 'array'); 13 | @if ($error) { @return $error; } 14 | 15 | $separator: $separator or list.separator($array); 16 | $brackets: $brackets or list.is-bracketed($array); 17 | 18 | @return list.join((), (), $separator, $brackets); 19 | } 20 | 21 | @function range( 22 | $start, 23 | $end, 24 | $step: 1, 25 | $include-end: true, 26 | $separator: null, 27 | $brackets: null 28 | ) { 29 | $range: template((), $separator, $brackets); 30 | $i: $start; 31 | 32 | @while if($include-end, $i <= $end, $i < $end) { 33 | $range: list.append($range, $i); 34 | $i: $i + $step; 35 | } 36 | 37 | @return $range; 38 | } 39 | 40 | @function from-string( 41 | $string, 42 | $separator: null, 43 | $limit: null 44 | ) { 45 | $list: (); 46 | 47 | @if (not $separator) or ($string == '') { 48 | @return list.append($list, $string); 49 | } 50 | 51 | $length: string.length($string); 52 | $limit: if($limit, math.min($limit, $length), $length); 53 | $index: string.index($string, $separator); 54 | 55 | @for $i from 1 through $limit { 56 | @if ($string) { 57 | $slice: null; 58 | 59 | @if ($separator == '') { 60 | $length: string.length($string); 61 | 62 | @if ($length > 1) { 63 | $slice: string.slice($string, 1, 1); 64 | $string: string.slice($string, 2); 65 | } @else { 66 | $slice: if(($length == 1), $string, ''); 67 | $string: null; 68 | } 69 | } @else if ($index) { 70 | $slice: string.slice($string, 1, $index - 1); 71 | $string: string.slice($string, $index + string.length($separator)); 72 | $index: string.index($string, $separator); 73 | $slice: if($string and not $index, $slice $string, $slice); 74 | } 75 | 76 | $list: if($slice, list.join($list, $slice), $list); 77 | } 78 | } 79 | 80 | @return $list; 81 | } 82 | 83 | @function slice( 84 | $array, 85 | $begin, 86 | $end: null 87 | ) { 88 | $error: throw.type($array, 'list' 'arglist', 'array.slice()', 'array'); 89 | @if ($error) { @return $error; } 90 | $error: throw.type($begin, 'number', 'array.slice()', 'begin'); 91 | @if ($error) { @return $error; } 92 | 93 | $length: list.length($array); 94 | $end: $end or $length; 95 | $end: if($end < 0, $length + $end, $end); 96 | 97 | $error: throw.type($end, 'number', 'array.slice()', 'end'); 98 | @if ($error) { @return $error; } 99 | 100 | $slice: template($array); 101 | 102 | @for $i from ($begin + 1) through math.min($end, $length) { 103 | $slice: list.append($slice, list.nth($array, $i)); 104 | } 105 | 106 | @return $slice; 107 | } 108 | 109 | @function join( 110 | $array, 111 | $separator: '' 112 | ) { 113 | $error: throw.type($array, 'list', 'array.join()', 'array'); 114 | @if ($error) { @return $error; } 115 | 116 | $return: ''; 117 | $first: true; 118 | @each $item in $array { 119 | $return: '#{$return}#{if($first, '', $separator)}#{$item}'; 120 | $first: false; 121 | } 122 | @return $return; 123 | } 124 | 125 | @function _args( 126 | $call: (), 127 | $index, 128 | $args... 129 | ) { 130 | $i: list.index($args, 'i') or list.index($args, 'index'); 131 | 132 | @if (list.length($args)) { 133 | @if ($i) { 134 | $args: list.set-nth($args, $i, $index); 135 | } 136 | 137 | $call: list.join($call, $args); 138 | } 139 | 140 | @return $call; 141 | } 142 | 143 | @function map( 144 | $array, 145 | $function, 146 | $args... 147 | ) { 148 | $error: throw.type($array, 'list', 'array.map()', 'array'); 149 | @if ($error) { @return $error; } 150 | 151 | $error: throw.type($function, 'function', 'array.map()', 'function'); 152 | @if ($error) { @return $error; } 153 | 154 | $mapped: template($array); 155 | $i: 1; 156 | 157 | @each $value in $array { 158 | $call: _args(($function, $value), $i, $args...); 159 | $mapped: list.append($mapped, meta.call($call...)); 160 | $i: $i + 1; 161 | } 162 | 163 | @return $mapped; 164 | } 165 | 166 | @function reduce( 167 | $array, 168 | $function, 169 | $accumulator: null, 170 | $args... 171 | ) { 172 | $error: throw.type($array, 'list', 'array.reduce()', 'array'); 173 | @if ($error) { @return $error; } 174 | 175 | $error: throw.type($function, 'function', 'array.reduce()', 'function'); 176 | @if ($error) { @return $error; } 177 | 178 | $i: 1; 179 | 180 | @each $value in $array { 181 | @if ($i == 1) and ($accumulator == null) { 182 | $accumulator: $value; 183 | } @else { 184 | $call: _args(($function, $accumulator, $value), $i, $args...); 185 | $accumulator: meta.call($call...); 186 | } 187 | 188 | $i: $i + 1; 189 | } 190 | 191 | @return $accumulator; 192 | } 193 | 194 | @function _add( 195 | $a, 196 | $b 197 | ) { 198 | @return $a + $b; 199 | } 200 | 201 | @function _subtract( 202 | $a, 203 | $b 204 | ) { 205 | @return $a - $b; 206 | } 207 | 208 | @function _multiply( 209 | $a, 210 | $b 211 | ) { 212 | @return $a * $b; 213 | } 214 | 215 | @function _divide( 216 | $a, 217 | $b 218 | ) { 219 | @return math.div($a, $b); 220 | } 221 | 222 | @function _math( 223 | $function, 224 | $a, 225 | $b 226 | ) { 227 | @return meta.call(get-function('_#{$function}'), $a, $b); 228 | } 229 | 230 | @function math( 231 | $function, 232 | $a, 233 | $b 234 | ) { 235 | $ta: meta.type-of($a); 236 | $tb: meta.type-of($b); 237 | 238 | @if ($ta == $tb) { 239 | @if ($ta == 'number') { 240 | @return _math($function, $a, $b); 241 | } 242 | 243 | @if ($ta == 'list') { 244 | $a-length: list.length($a); 245 | $b-length: list.length($b); 246 | 247 | @if ($a-length == $b-length) { 248 | $result: template($a); 249 | 250 | @for $i from 1 through $a-length { 251 | $sum: _math($function, list.nth($a, $i), list.nth($b, $i)); 252 | $result: list.append($result, $sum); 253 | } 254 | 255 | @return $result; 256 | } 257 | 258 | // treat single-item lists as single values 259 | @if ($a-length == 1) { 260 | @return math($function, nth($a, 1), $b); 261 | } 262 | 263 | @if ($b-length == 1) { 264 | @return math($function, $a, nth($b, 1)); 265 | } 266 | 267 | @return throw.error( 268 | '$a and $b must have an equal length if both are lists', 269 | 'array.add()' 270 | ); 271 | } 272 | } 273 | 274 | $tab: ($ta, $tb); 275 | $ni: list.index($tab, 'number'); 276 | $li: list.index($tab, 'list'); 277 | 278 | @if ($ni and $li) { 279 | $ab: ($a, $b); 280 | $n: list.nth($ab, $ni); 281 | $l: list.nth($ab, $li); 282 | $result: template($l); 283 | 284 | @each $item in $l { 285 | $result: list.append($result, _math($function, $item, $n)); 286 | } 287 | 288 | @return $result; 289 | } 290 | 291 | @return throw.error( 292 | 'Failed to #{$function} values: `#{$a}` and `#{$b}`', 293 | 'array.add()' 294 | ); 295 | } 296 | 297 | @function add( 298 | $a, 299 | $b 300 | ) { 301 | @return math('add', $a, $b); 302 | } 303 | 304 | @function multiply( 305 | $a, 306 | $b 307 | ) { 308 | @return math('multiply', $a, $b); 309 | } 310 | 311 | @function sum( 312 | $array 313 | ) { 314 | $error: throw.type($array, 'list', 'array.sum()', 'array'); 315 | @if ($error) { @return $error; } 316 | 317 | @return reduce($array, get-function('add'), 0); 318 | } 319 | 320 | @function average( 321 | $array 322 | ) { 323 | $error: throw.type($array, 'list', 'array.average()', 'array'); 324 | @if ($error) { @return $error; } 325 | 326 | @return math.div(sum($array), list.length($array)); 327 | } 328 | 329 | @function product( 330 | $array 331 | ) { 332 | $error: throw.type($array, 'list', 'array.sum()', 'array'); 333 | @if ($error) { @return $error; } 334 | 335 | @return reduce($array, get-function('multiply')); 336 | } 337 | -------------------------------------------------------------------------------- /sass/utils/_channel.scss: -------------------------------------------------------------------------------- 1 | @use 'array'; 2 | @use 'throw'; 3 | @use 'sass:list'; 4 | @use 'sass:math'; 5 | @use 'sass:meta'; 6 | 7 | // half-open 8 | @function deg6( 9 | $val 10 | ) { 11 | @return math.div((($val + 1) % 360) - 1, 60); 12 | } 13 | 14 | // fractions 15 | // --------- 16 | 17 | @function fraction( 18 | $value, 19 | $range: 100, 20 | ) { 21 | @return math.div($value, $range); 22 | } 23 | 24 | @function fractions( 25 | $values, 26 | $range: 100, 27 | ) { 28 | @return array.map($values, meta.get-function('fraction'), $range); 29 | } 30 | 31 | // Format 32 | // ------ 33 | @function _format( 34 | $val, 35 | $i, 36 | $units, 37 | $decimals, 38 | ) { 39 | $unit: if(meta.type-of($units) == 'number', $units, list.nth($units, $i)); 40 | $decimals: if($decimals, math.pow(10, $decimals), 1); 41 | @return math.div(math.round($val * $unit * $decimals), $decimals); 42 | } 43 | 44 | @function format( 45 | $channels, 46 | $units: 100%, 47 | $decimals: 2, 48 | ) { 49 | @return array.map( 50 | $channels, 51 | meta.get-function('_format'), 52 | i, 53 | $units, 54 | $decimals 55 | ); 56 | } 57 | 58 | // Validation 59 | // ---------- 60 | 61 | @function valid( 62 | $channels, 63 | $source, 64 | $name, 65 | $count: 3 66 | ) { 67 | // type-of list 68 | $error: throw.type($channels, 'list', $source, $name); 69 | @if ($error) { @return $error; } 70 | 71 | // length == $count 72 | $length: list.length($channels); 73 | @if ($length != $count) { 74 | @return throw.error( 75 | '$#{$name} expects #{$count} channels, got #{$length}', 76 | $source 77 | ); 78 | } 79 | 80 | // each channel is a number 81 | @each $c in $channels { 82 | $c-type: meta.type-of($c); 83 | @if ($c-type != 'number') { 84 | @return throw.type($c, 'number', $source, '#{$name} channels'); 85 | } 86 | } 87 | 88 | @return $channels; 89 | } 90 | -------------------------------------------------------------------------------- /sass/utils/_matrix.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:list'; 2 | @use 'sass:meta'; 3 | @use 'sass:string'; 4 | @use 'array'; 5 | @use 'throw'; 6 | 7 | // Inspection 8 | 9 | @function is-2d( 10 | $matrix 11 | ) { 12 | @return if( 13 | list.length($matrix) > 0, 14 | (meta.type-of(list.nth($matrix, 1)) == 'list'), 15 | false 16 | ); 17 | } 18 | 19 | @function force-2d( 20 | $matrix 21 | ) { 22 | @return if( 23 | is-2d($matrix), 24 | $matrix, 25 | list.append( 26 | array.template($matrix), 27 | $matrix 28 | ) 29 | ); 30 | } 31 | 32 | // Validation 33 | 34 | @function _invalid-items( 35 | $matrix, 36 | $list: true 37 | ) { 38 | $item-error: false; 39 | 40 | @if (list.length($matrix) > 0) { 41 | $first: list.nth($matrix, 1); 42 | $first-type: meta.type-of($first); 43 | $length: if($first-type == 'list', list.length($first), null); 44 | 45 | @each $item in $matrix { 46 | $type: meta.type-of($item); 47 | $item-error: $item-error or ($type != $first-type); 48 | 49 | @if ($type == 'list') and ($list) { 50 | $length-error: list.length($item) != $length; 51 | $item-error: $item-error or $length-error or _invalid-items($item, false); 52 | } @else if ($type != 'number') { 53 | $item-error: true; 54 | } 55 | } 56 | } 57 | 58 | @return $item-error; 59 | } 60 | 61 | @function is-valid( 62 | $matrix, 63 | $error: true 64 | ) { 65 | // must be a list 66 | $type-error: throw.type($matrix, 'list', 'matrix.is-valid()', 'matrix'); 67 | @if ($type-error) { @return if($error, $type-error, false); } 68 | 69 | @if (_invalid-items($matrix)) { 70 | $item-error: throw.error( 71 | 'Matrix items must be numbers, or equal-length lists of numbers', 72 | 'matrix.is-valid()', 73 | ); 74 | @if ($item-error) { @return if($error, $item-error, false); } 75 | } 76 | 77 | @return $matrix; 78 | } 79 | 80 | // types 81 | 82 | @function is( 83 | $value, 84 | $expect: null 85 | ) { 86 | $type: meta.type-of($value); 87 | 88 | @if ($type == 'number') { 89 | $type: 'scalar'; 90 | } @else if ($type == 'list') and (is-valid($value)) { 91 | @if (is-2d($value)) { 92 | $type: 'matrix'; 93 | } @else { 94 | $type: 'vector'; 95 | } 96 | } @else { 97 | @return throw.error( 98 | '$value should be a scalar, matrix, or vector. Got: #{$value}', 99 | 'matrix.is()' 100 | ); 101 | } 102 | 103 | @return if($expect, ($type == $expect), $type); 104 | } 105 | 106 | // Methods 107 | 108 | @function size( 109 | $matrix, 110 | $get: false 111 | ) { 112 | $is: is($matrix); 113 | 114 | @if ($is == 'scalar') { 115 | @return if($get, 0, []); 116 | } 117 | 118 | @if ($is == 'matrix') { 119 | $rows: list.length($matrix); 120 | $columns: list.length(nth($matrix, 1)); 121 | 122 | @if ($get == 'columns') { 123 | @return $columns; 124 | } @else if ($get == 'rows') { 125 | @return $rows; 126 | } 127 | 128 | @return [ $rows, $columns ]; 129 | } 130 | 131 | $length: list.length($matrix); 132 | 133 | @if ($get == 'columns') { 134 | @return $length; 135 | } @else if ($get == 'rows') { 136 | @return 0; 137 | } 138 | 139 | @return [$length]; 140 | } 141 | 142 | // Alterations 143 | 144 | @function squeeze( 145 | $matrix 146 | ) { 147 | $rows: size($matrix, 'rows'); 148 | 149 | @if ($rows == 1) { 150 | @return squeeze(list.nth($matrix, 1)); 151 | } 152 | 153 | @return $matrix; 154 | } 155 | 156 | @function transpose( 157 | $matrix 158 | ) { 159 | $matrix: force-2d(is-valid($matrix)); 160 | $trans: array.template($matrix); 161 | $row-template: array.template(list.nth($matrix, 1)); 162 | 163 | @for $i from 1 through list.length(list.nth($matrix, 1)) { 164 | $row: $row-template; 165 | 166 | @each $list in $matrix { 167 | $row: list.append($row, list.nth($list, $i)); 168 | } 169 | 170 | $trans: list.append($trans, $row); 171 | } 172 | 173 | @return $trans; 174 | } 175 | 176 | // Access 177 | 178 | @function get( 179 | $matrix, 180 | $index 181 | ) { 182 | $matrix: is-valid($matrix); 183 | $index: if(meta.type-of($index) != 'list', ($index), $index); 184 | 185 | $found: $matrix; 186 | 187 | @each $i in $index { 188 | $found: list.nth($found, $i); 189 | } 190 | 191 | @return $found; 192 | } 193 | 194 | @function set( 195 | $matrix, 196 | $index, 197 | $value 198 | ) { 199 | $matrix: is-valid($matrix); 200 | 201 | @if (meta.type-of($index) == 'list') { 202 | @if (list.length($index) > 1) { 203 | $i: list.nth($index, 1); 204 | $value: set( 205 | list.nth($matrix, $i), 206 | array.slice($index, 1), 207 | $value 208 | ); 209 | $index: $i; 210 | } @else { 211 | $index: nth($index, 1); 212 | } 213 | } 214 | 215 | @return list.set-nth($matrix, $index, $value); 216 | } 217 | 218 | // Math 219 | 220 | @function _scalar-multiply( 221 | $matrix, 222 | $scalar, 223 | ) { 224 | @if (is-2d($matrix)) { 225 | $product: array.template($matrix); 226 | 227 | @each $array in $matrix { 228 | $row: array.multiply($array, $scalar); 229 | $product: list.append($product, $row); 230 | } 231 | 232 | @return $product; 233 | } 234 | 235 | @return array.multiply($matrix, $scalar); 236 | } 237 | 238 | @function _matrix-multiply( 239 | $left, 240 | $right 241 | ) { 242 | // validate 243 | $right: transpose($right); 244 | $product: array.template($left); 245 | 246 | @each $l-row in $left { 247 | $row: array.template($l-row); 248 | @each $r-row in $right { 249 | $sum: array.sum(array.multiply($l-row, $r-row)); 250 | $row: list.append($row, $sum); 251 | } 252 | $product: list.append($product, $row); 253 | } 254 | 255 | @return $product; 256 | } 257 | 258 | @function multiply( 259 | $left, 260 | $right 261 | ) { 262 | $lis: is($left); 263 | $ris: is($right); 264 | $lris: ($lis $ris); 265 | 266 | // handle matched-value math 267 | @if ($lis == $ris) { 268 | @if ($lis == 'scalar') { 269 | @return $left * $right; 270 | } @else if ($lis == 'vector') { 271 | @return array.multiply($left, $right); 272 | } 273 | 274 | @if (size($left, 'columns') != size($right, 'rows')) { 275 | @return throw.error( 276 | 'Left matrix columns must equal right matrix rows', 277 | 'matrix.multiply()' 278 | ); 279 | } 280 | 281 | @return _matrix-multiply($left, $right); 282 | } 283 | 284 | // handle scalar math 285 | $si: list.index($lris, 'scalar'); 286 | 287 | @if ($si) { 288 | $mi: if(($si == 1), 2, 1); 289 | $lr: ($left $right); 290 | 291 | @return _scalar-multiply( 292 | list.nth($lr, $mi), 293 | list.nth($lr, $si) 294 | ); 295 | } 296 | 297 | // left vectors with right matrix 298 | @if ($lis == 'vector') { 299 | @if (list.length($left) != size($right, 'rows')) { 300 | @return throw.error( 301 | 'Left vector length must equal right matrix rows', 302 | 'matrix.multiply()' 303 | ); 304 | } 305 | 306 | @return squeeze( 307 | _matrix-multiply( 308 | force-2d($left), 309 | $right 310 | ) 311 | ); 312 | } 313 | 314 | // right vectors with left matrix 315 | @if (size($left, 'columns') != list.length($right)) { 316 | @return throw.error( 317 | 'Left matrix columns must equal right vector length', 318 | 'matrix.multiply()' 319 | ); 320 | } 321 | 322 | $product: _matrix-multiply($left, transpose($right)); 323 | @for $i from 1 through list.length($product) { 324 | $product: list.set-nth( 325 | $product, 326 | $i, 327 | list.nth( 328 | list.nth($product, $i), 329 | 1 330 | ) 331 | ); 332 | } 333 | @return $product; 334 | } 335 | -------------------------------------------------------------------------------- /sass/utils/_pow.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:list'; 2 | @use 'sass:math'; 3 | @use 'sass:meta'; 4 | @use 'array'; 5 | 6 | @function cbrt( 7 | $n 8 | ) { 9 | @if (meta.type-of($n) == 'list') { 10 | $list: array.template($n); 11 | 12 | @each $item in $n { 13 | $list: list.append($list, cbrt($item)); 14 | } 15 | 16 | @return $list; 17 | } 18 | 19 | @if ($n < 0) { 20 | @return math.pow(math.abs($n), math.div(1, 3)) * -1; 21 | } 22 | 23 | @return math.pow($n, math.div(1, 3)); 24 | } 25 | -------------------------------------------------------------------------------- /sass/utils/_relative.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:list'; 2 | @use 'sass:map'; 3 | @use 'sass:meta'; 4 | @use 'sass:string'; 5 | @use 'array'; 6 | 7 | @function _channel( 8 | $val, 9 | $i, 10 | $values, 11 | ) { 12 | @if (meta.type-of($val) == 'number') { 13 | @return $val; 14 | } 15 | 16 | $is: map.get($values, $val); 17 | 18 | @if ($is) { 19 | @return $is; 20 | } 21 | 22 | @return null; 23 | 24 | @if string.index($val, 'calc(') { 25 | $calc: array.from-string(string.slice($val, 6, -2), ' '); 26 | } 27 | } 28 | 29 | @function _values( 30 | $all, 31 | $val, 32 | $i, 33 | $format 34 | ) { 35 | @return map.merge($all, (string.slice($format, $i, $i): $val)); 36 | } 37 | 38 | @function compile( 39 | $channels, 40 | $source, 41 | $format 42 | ) { 43 | @return array.map( 44 | $channels, 45 | meta.get-function('_channel'), 46 | i, 47 | array.reduce($source, meta.get-function('_values'), (), i, $format) 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /sass/utils/_throw.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:list'; 2 | @use 'sass:meta'; 3 | 4 | // Error Output Override 5 | // --------------------- 6 | /// Optionally turn off error output for testing, 7 | /// with the 'catch-errors' setting 8 | /// @access private 9 | $catch-errors: false !default; 10 | 11 | 12 | // Error [function] 13 | // ---------------- 14 | /// Optionally return error messages without failing, 15 | /// as a way to test error cases 16 | /// 17 | /// @param {string} $message - 18 | /// A useful error message, explaining the problem 19 | /// @param {string} $source - 20 | /// The original source of the error for debugging 21 | /// @param {bool} $catch [$catch-errors] - 22 | /// Optionally return the error rather than failing 23 | /// @return {string} - 24 | /// Combined error with source and message 25 | /// @throws When `$catch == true` 26 | /// @access private 27 | @function error( 28 | $message, 29 | $source, 30 | $catch: $catch-errors 31 | ) { 32 | @if $catch { 33 | @return 'ERROR [#{$source}] #{$message}'; 34 | } 35 | 36 | @error '[#{$source}] #{$message}'; 37 | } 38 | 39 | 40 | @function type( 41 | $value, 42 | $expect, 43 | $source, 44 | $name 45 | ) { 46 | $type: meta.type-of($value); 47 | $pass: if( 48 | meta.type-of($expect) == 'list', 49 | list.index($expect, $type), 50 | $type == $expect 51 | ); 52 | 53 | @if (not $pass) { 54 | @if (meta.type-of($expect) == 'list') { 55 | $expect: 'one of (#{list.join((), $expect, comma)})'; 56 | } @else { 57 | $expect: 'a #{$expect}'; 58 | } 59 | 60 | @return error( 61 | '$#{$name} must be #{$expect}, got #{$type}: #{$value}', 62 | $source 63 | ); 64 | } 65 | 66 | @return false; 67 | } 68 | 69 | 70 | // Error [mixin] 71 | // ------------- 72 | /// Optionally output mixin error messages without failing, 73 | /// as a way to test error cases 74 | /// 75 | /// @param {string} $message - 76 | /// A useful error message, explaining the problem 77 | /// @param {string} $source - 78 | /// The original source of the error for debugging 79 | /// @param {bool} $catch [$catch-errors] - 80 | /// Optionally return the error rather than failing 81 | /// @output - 82 | /// `--accoutrement-error` property with error message 83 | /// @access private 84 | @mixin error( 85 | $message, 86 | $source, 87 | $catch: $catch-errors 88 | ) { 89 | --accoutrement-error: '#{error($message, $source, $catch)}'; 90 | } 91 | -------------------------------------------------------------------------------- /sass/utils/_units.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | @use 'sass:meta'; 4 | @use 'array'; 5 | 6 | @function strip($x) { 7 | @return math.div($x, $x * 0 + 1); 8 | } 9 | 10 | @function strip-all( 11 | $array 12 | ) { 13 | @return array.map( 14 | $array, 15 | meta.get-function('strip') 16 | ); 17 | } 18 | 19 | @function to-degrees( 20 | $angle 21 | ) { 22 | @return (0deg + $angle) % 360; 23 | } 24 | -------------------------------------------------------------------------------- /src/json.css: -------------------------------------------------------------------------------- 1 | /*! json-encode: {"colors": {"cie-lch": {"lch": "rgb(102, 51, 153)", "lch-a": "rgba(102, 51, 153, 0.6)"}, "cie-lab": {"lab": "rgb(102, 51, 153)", "lab-a": "rgba(102, 51, 153, 0.6)"}, "inspect-contrast": {"papayawhip": "papayawhip", "default": "black", "highest": "maroon", "first": "rebeccapurple"}, "inspect-cie": {"papayawhip": "papayawhip", "rebeccapurple": "rebeccapurple", "yellow": "yellow", "deeppink": "deeppink"}, "adjust-from": {"rebeccapurple": "rebeccapurple", "set chroma 20": "rgb(87, 70, 101)", "adjust hue -60": "rgb(0, 82, 120)", "scale lightness 50%": "rgb(195, 136, 248)", "multiply lightness * 2": "rgb(191, 133, 244)"}}} */ 2 | 3 | /*# sourceMappingURL=json.css.map */ 4 | -------------------------------------------------------------------------------- /src/json.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["../node_modules/sassdoc-theme-herman/scss/utilities/_json-api.scss"],"names":[],"mappings":"AA8NE","file":"json.css"} -------------------------------------------------------------------------------- /src/json.scss: -------------------------------------------------------------------------------- 1 | @use '../sass/blend'; 2 | @use '../node_modules/sassdoc-theme-herman/scss/utilities' as *; 3 | 4 | // CIE 5 | @include add('colors', 'cie-lch', ( 6 | 'lch': blend.lch(32.39% 61.25 308.86deg), 7 | 'lch-a': blend.lch(32.39% 61.25 308.86deg, 60%), 8 | )); 9 | 10 | @include add('colors', 'cie-lab', ( 11 | 'lab': blend.lab(32.39% 38.43 -47.69), 12 | 'lab-a': blend.lab(32.39% 38.43 -47.69, 0.6), 13 | )); 14 | 15 | // Inspect 16 | @include add('colors', 'inspect-contrast', ( 17 | 'papayawhip': papayawhip, 18 | 'default': blend.contrast(papayawhip), 19 | 'highest': blend.contrast(papayawhip, rebeccapurple, maroon), 20 | 'first': blend.contrast(papayawhip, rebeccapurple, maroon, 4.5), 21 | )); 22 | 23 | @include add('colors', 'inspect-cie', ( 24 | 'papayawhip': papayawhip, 25 | 'rebeccapurple': rebeccapurple, 26 | 'yellow': yellow, 27 | 'deeppink': deeppink, 28 | )); 29 | 30 | // Adjust 31 | @include add('colors', 'adjust-from', ( 32 | 'rebeccapurple': rebeccapurple, 33 | 'set chroma 20': blend.from(rebeccapurple, l, 20, h), 34 | 'adjust hue -60': blend.from(rebeccapurple, l, c, h -60), 35 | 'scale lightness 50%': blend.from(rebeccapurple, l 50%, c, h), 36 | 'multiply lightness * 2': blend.from(rebeccapurple, 2l, c, h), 37 | )); 38 | 39 | @include export($herman); 40 | -------------------------------------------------------------------------------- /test/adjust/_channels.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/adjust/channels'; 3 | 4 | @include describe('adjust/channels') { 5 | @include describe('set()') { 6 | @include it('Returns a color with set value on a channel') { 7 | @include assert-equal( 8 | channels.set(rebeccapurple, $chroma: 20), 9 | #574665 10 | ); 11 | } 12 | } 13 | 14 | @include describe('adjust()') { 15 | @include it('Returns a color with adjusted value on a channel') { 16 | @include assert-equal( 17 | channels.adjust(rebeccapurple, $h: -60), 18 | #005278 19 | ); 20 | } 21 | } 22 | 23 | @include describe('scale()') { 24 | @include it('Returns a color with scaled value on a channel') { 25 | @include assert-equal( 26 | channels.scale(rebeccapurple, $lightness: 50%), 27 | #c388f8 28 | ); 29 | } 30 | 31 | @include it('Errors when given non-percent values') { 32 | @include assert-equal( 33 | channels.scale(red, $l: 20), 34 | 'ERROR [scale()] Scales must be defined using `%` units' 35 | ); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/adjust/_from.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/adjust/from' as adjust; 3 | @use '../../sass/inspect'; 4 | @use '../../sass/cie'; 5 | 6 | @include describe('adjust/from') { 7 | @include describe('from()') { 8 | @include it('Can replace a channel') { 9 | @include assert-equal( 10 | adjust.from(maroon, l, 0, h), 11 | #3e3e3e 12 | ); 13 | @include assert-equal( 14 | adjust.from(maroon, 100%, c, h), 15 | white 16 | ); 17 | } 18 | 19 | @include it('Retains the alpha channel') { 20 | @include assert-equal( 21 | adjust.from(rgba(maroon, 0.5), l, 0, h), 22 | rgba(#3e3e3e, 0.5) 23 | ); 24 | } 25 | 26 | @include it('Can multiply a channel') { 27 | @include assert-equal( 28 | adjust.from(cie.lch(30% 0 0), 2l), 29 | cie.lch(60% 0 0) 30 | ); 31 | } 32 | 33 | @include it('Can perform linear adjustments') { 34 | @include assert-equal( 35 | adjust.from(cie.lch(30% 25 300), l, c -10), 36 | cie.lch(30% 15 300) 37 | ); 38 | } 39 | 40 | @include it('Can perform scaled adjustments') { 41 | @include assert-equal( 42 | adjust.from(cie.lch(30% 0 300), l 50%, c, h), 43 | cie.lch(65% 0 300) 44 | ); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/adjust/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'utils'; 2 | @forward 'parse'; 3 | @forward 'from'; 4 | @forward 'channels'; 5 | -------------------------------------------------------------------------------- /test/adjust/_parse.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/adjust/parse'; 3 | @use 'sass:meta'; 4 | 5 | @function compliment( 6 | $val 7 | ) { 8 | @return ($val + 180) % 360; 9 | } 10 | 11 | @include describe('adjust/parse') { 12 | @include describe('from()') { 13 | @include it('Returns the given value for a channel key') { 14 | @include assert-equal( 15 | parse.from(50, l, l), 16 | 50 17 | ); 18 | @include assert-equal( 19 | parse.from(50, c, c), 20 | 50 21 | ); 22 | @include assert-equal( 23 | parse.from(50, h, h), 24 | 50 25 | ); 26 | } 27 | 28 | @include it('Replace value with a unitless number') { 29 | @include assert-equal( 30 | parse.from(60, 20, l), 31 | 20 32 | ); 33 | } 34 | 35 | @include it('Multiplies key-unit values') { 36 | @include assert-equal( 37 | parse.from(40, 2l, l), 38 | 80 39 | ); 40 | @include assert-equal( 41 | parse.from(15, 2c, c), 42 | 30 43 | ); 44 | } 45 | 46 | @include it('Can pass the value to a simple function') { 47 | @include assert-equal( 48 | parse.from(210, meta.get-function('compliment'), h), 49 | 30 50 | ); 51 | } 52 | 53 | @include it('Handles linear adjustments') { 54 | @include assert-equal( 55 | parse.from(60, l 20, l), 56 | 80 57 | ); 58 | @include assert-equal( 59 | parse.from(60, l -20, l), 60 | 40 61 | ); 62 | } 63 | 64 | @include it('Handles scaled relative adjustments') { 65 | @include assert-equal( 66 | parse.from(60, l 20%, l), 67 | 68 68 | ); 69 | @include assert-equal( 70 | parse.from(60, l -20%, l), 71 | 48 72 | ); 73 | } 74 | } 75 | 76 | @include describe('args()') { 77 | @include it('Returns normalized lch args if given chroma or hue') { 78 | @include assert-equal( 79 | parse.args((lightness: 25, hue: 30), 'set'), 80 | (l: 25, c: null, h: 30deg) 81 | ); 82 | 83 | @include assert-equal( 84 | parse.args((chroma: 30), 'set'), 85 | (l: null, c: 30, h: null), 86 | ); 87 | } 88 | 89 | @include it('Accepts shorthand LCH channel names') { 90 | @include assert-equal( 91 | parse.args((l: 25, c: 35, h: 45), 'set'), 92 | (l: 25, c: 35, h: 45deg), 93 | ); 94 | } 95 | 96 | @include it('Returns Lab vales if c/h are not given') { 97 | @include assert-equal( 98 | parse.args((l: 25), 'set'), 99 | (l: 25, a: null, b: null) 100 | ); 101 | } 102 | 103 | @include it('Returns Lab vales if a/b are given') { 104 | @include assert-equal( 105 | parse.args((a: 25), 'set'), 106 | (l: null, a: 25, b: null) 107 | ); 108 | 109 | @include assert-equal( 110 | parse.args((lightness: 30, b: 25), 'set'), 111 | (l: 30, a: null, b: 25) 112 | ); 113 | } 114 | 115 | @include it('Errors when given both Lab & LCH args') { 116 | @include assert-equal( 117 | parse.args((c: 20, b: 30), 'set'), 118 | 'ERROR [set()] Cannot set both Lab ($a, $b) & LCH ($c, $h) channels at once' 119 | ); 120 | @include assert-equal( 121 | parse.args((h: 20, a: 30), 'set'), 122 | 'ERROR [set()] Cannot set both Lab ($a, $b) & LCH ($c, $h) channels at once' 123 | ); 124 | } 125 | } 126 | 127 | @include describe('do()') { 128 | @include it('Performs set() adjustments on a color') { 129 | @include assert-equal( 130 | parse.do('set', rebeccapurple, (l: null, c: 20, h: null)), 131 | #574665 132 | ); 133 | } 134 | 135 | @include it('Performs adjust() adjustments on a color') { 136 | @include assert-equal( 137 | parse.do('adjust', rebeccapurple, (l: null, c: null, h: -60)), 138 | #005278 139 | ); 140 | } 141 | 142 | @include it('Performs scale() adjustments on a color') { 143 | @include assert-equal( 144 | parse.do('scale', rebeccapurple, (l: 50%, a: null, b: null)), 145 | #c388f8 146 | ); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /test/adjust/_utils.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/adjust/utils'; 3 | 4 | @include describe('adjust/utils') { 5 | @include describe('set()') { 6 | @include it('Returns the second value') { 7 | @include assert-equal( 8 | utils.set(5, 10), 9 | 10 10 | ); 11 | } 12 | } 13 | 14 | @include describe('adjust()') { 15 | @include it('Adds two values together') { 16 | @include assert-equal( 17 | utils.adjust(5, 10), 18 | 15 19 | ); 20 | @include assert-equal( 21 | utils.adjust(15, -10), 22 | 5 23 | ); 24 | } 25 | } 26 | 27 | @include describe('scale()') { 28 | @include it('Scales degrees by a fraction of the hue wheel') { 29 | @include assert-equal( 30 | utils.scale(90, 50%, 360), 31 | 270 32 | ); 33 | } 34 | 35 | @include it('Scales negative degrees') { 36 | @include assert-equal( 37 | utils.scale(90, -20%, 360), 38 | 18 39 | ); 40 | } 41 | 42 | @include it('Continues around hue wheel') { 43 | @include assert-equal( 44 | utils.scale(180, 75%, 360), 45 | 90 46 | ); 47 | } 48 | 49 | @include it('Scales up within the default range (0, 100)') { 50 | @include assert-equal( 51 | utils.scale(90, 50%), 52 | 95 53 | ); 54 | } 55 | 56 | @include it('Scales down within the default range (0, 100)') { 57 | @include assert-equal( 58 | utils.scale(60, -50%), 59 | 30 60 | ); 61 | } 62 | 63 | @include it('Handles arbitrary ranges like Lab a/b (-160, 160)') { 64 | @include assert-equal( 65 | utils.scale(60, -25%, -160 160), 66 | 5, 67 | ); 68 | @include assert-equal( 69 | utils.scale(60, 25%, -160 160), 70 | 85, 71 | ); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/cie/_api.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/cie'; 3 | @use 'sass:string'; 4 | 5 | @include describe('cie/api') { 6 | @include describe('lch()') { 7 | @include it('Returns a Sass color from lch/a values') { 8 | @include assert-equal( 9 | cie.lch(30% 50 300deg, 50%), 10 | rgba(77, 58, 139, 0.5) 11 | ); 12 | } 13 | 14 | @include it('Accepts hue in non-degree angle units') { 15 | @include assert-equal( 16 | cie.lch(30% 50 180deg), 17 | cie.lch(30% 50 0.5turn) 18 | ); 19 | @include assert-equal( 20 | cie.lch(30% 50 3.1416rad), 21 | cie.lch(30% 50 180deg) 22 | ); 23 | @include assert-equal( 24 | cie.lch(30% 50 200grad), 25 | cie.lch(30% 50 180deg) 26 | ); 27 | } 28 | } 29 | 30 | @include describe('lab()') { 31 | @include it('Returns a Sass color from lab/a values') { 32 | @include assert-equal( 33 | cie.lab(30% 50 -50, 50%), 34 | rgba(111, 28, 151, 0.5) 35 | ); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/cie/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'lch'; 2 | @forward 'api'; 3 | -------------------------------------------------------------------------------- /test/cie/_lch.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/cie/lch'; 3 | @use '../../sass/convert'; 4 | @use 'sass:meta'; 5 | @use 'sass:string'; 6 | 7 | @include describe('cie/lch') { 8 | @include describe('normalize') { 9 | @include it('Strips all units from an LCH value') { 10 | @include assert-equal( 11 | lch.normalize(30% 0 300deg), 12 | 30 0 300 13 | ); 14 | } 15 | 16 | @include it('Converts hue to degrees before normalizing') { 17 | @include assert-equal( 18 | lch.normalize(30% 0 0.5turn), 19 | 30 0 180 20 | ); 21 | } 22 | } 23 | 24 | @include describe('to-srgb()') { 25 | @include it('Converts LCH to rgb') { 26 | @include assert-equal( 27 | lch.to-srgb(50 50 0), 28 | convert.LCH_to_sRGB(50 50 0) 29 | ); 30 | } 31 | 32 | @include it('Optionally returns null for out-of-gamut LCH colors') { 33 | @include assert-equal( 34 | lch.to-srgb(80 80 300, $check-gamut: true), 35 | null 36 | ); 37 | } 38 | } 39 | 40 | @include describe('gamut-correct()') { 41 | @include it('Returns converted sRGB for in-gamut colors') { 42 | @include assert-equal( 43 | lch.gamut-correct(30, 50, 300), 44 | convert.LCH_to_sRGB(30 50 300) 45 | ); 46 | } 47 | 48 | @include it('Returns corrected sRGB for out-of-gamut colors') { 49 | @include assert-equal( 50 | lch.gamut-correct(80, 80, 300), 51 | [0.8224699758, 0.7397233782, 1.0000048368], 52 | $inspect: true 53 | ); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/convert/_conversions.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/convert/conversions'; 3 | 4 | @include describe('convert/conversions') { 5 | @include describe('lin_sRGB()') { 6 | @include it('converts rgb channels to linear-light') { 7 | @include assert-equal( 8 | conversions.lin_sRGB([ 0.02, 0.5, 0.8 ]), 9 | [ 0.0015479876160990713, 0.21404114048223255, 0.6038273388553378 ] 10 | ) 11 | } 12 | } 13 | 14 | @include describe('gam_sRGB()') { 15 | @include it('gamma-corrects linear-light rgb channels') { 16 | @include assert-equal( 17 | conversions.gam_sRGB([ 0.02, 0.5, 0.8 ]), 18 | [ 0.15170371931624205, 0.7353569830524495, 0.9063317533440594 ] 19 | ) 20 | } 21 | } 22 | 23 | @include describe('lin_sRGB_to_XYZ()') { 24 | @include it('converts linear-light rgb values to CIE XYZ') { 25 | @include assert-equal( 26 | conversions.lin_sRGB_to_XYZ([ 0.02, 0.5, 0.8 ]), 27 | [0.3314246164, 0.4195909718, 0.8204097281], 28 | $inspect: true 29 | ) 30 | } 31 | } 32 | 33 | @include describe('XYZ_to_lin_sRGB()') { 34 | @include it('convert XYZ to linear-light sRGB') { 35 | @include assert-equal( 36 | conversions.XYZ_to_lin_sRGB([ 0.02, 0.5, 0.8 ]), 37 | [-1.1027607982, 0.951842924, 0.7447013335], 38 | $inspect: true 39 | ) 40 | } 41 | } 42 | 43 | @include describe('lin_P3_to_XYZ()') { 44 | @include it('convert linear-light display-p3 values to CIE XYZ') { 45 | @include assert-equal( 46 | conversions.lin_P3_to_XYZ([ 0.02, 0.5, 0.8 ]), 47 | [ 0.3011390937450008, 0.41387828347464417, 0.8577121860502321 ] 48 | ) 49 | } 50 | } 51 | 52 | @include describe('XYZ_to_lin_P3()') { 53 | @include it('convert XYZ to linear-light P3') { 54 | @include assert-equal( 55 | conversions.XYZ_to_lin_P3([ 0.02, 0.5, 0.8 ]), 56 | [ -0.737990498281307, 0.8836419994414965, 0.7281383411770046 ] 57 | ) 58 | } 59 | } 60 | 61 | @include describe('lin_ProPhoto()') { 62 | @include it('convert an array of prophoto-rgb values to linear light') { 63 | @include assert-equal( 64 | conversions.lin_ProPhoto([ 0.02, 0.5, 0.8 ]), 65 | [ 0.00125, 0.2871745887492587, 0.669209313658415 ] 66 | ) 67 | } 68 | } 69 | 70 | @include describe('gam_ProPhoto()') { 71 | @include it('gamma-correct an array of linear-light prophoto-rgb') { 72 | @include assert-equal( 73 | conversions.gam_ProPhoto([ 0.02, 0.5, 0.8 ]), 74 | [ 0.11379620405527816, 0.6803950000871885, 0.8834075444455188 ] 75 | ) 76 | } 77 | } 78 | 79 | @include describe('lin_ProPhoto_to_XYZ()') { 80 | @include it('converts linear-light prophoto-rgb values to CIE XYZ') { 81 | @include assert-equal( 82 | conversions.lin_ProPhoto_to_XYZ([ 0.02, 0.5, 0.8 ]), 83 | [ 0.10862760804653605, 0.3617515546381208, 0.6600836820083682 ] 84 | ) 85 | } 86 | } 87 | 88 | @include describe('XYZ_to_lin_ProPhoto()') { 89 | @include it('converts XYZ to linear-light prophoto-rgb') { 90 | @include assert-equal( 91 | conversions.XYZ_to_lin_ProPhoto([ 0.02, 0.5, 0.8 ]), 92 | [ -0.14175909863195832, 0.7596527466917661, 0.9695740365111564 ] 93 | ) 94 | } 95 | } 96 | 97 | @include describe('lin_a98rgb()') { 98 | @include it('converts a98-rgb values to linear-light') { 99 | @include assert-equal( 100 | conversions.lin_a98rgb([ 0.02, 0.5, 0.8 ]), 101 | [ 0.00018348193467219382, 0.21775552814439456, 0.6121723111134431 ] 102 | ) 103 | } 104 | } 105 | 106 | @include describe('gam_a98rgb()') { 107 | @include it('gamma-corrects an array of linear-light a98-rgb') { 108 | @include assert-equal( 109 | conversions.gam_a98rgb([ 0.02, 0.5, 0.8 ]), 110 | [ 0.16883658916733524, 0.7296583817678015, 0.9035128753395815 ] 111 | ) 112 | } 113 | } 114 | 115 | @include describe('lin_a98rgb_to_XYZ()') { 116 | @include it('converts linear-light a98-rgb values to CIE XYZ') { 117 | @include assert-equal( 118 | conversions.lin_a98rgb_to_XYZ([ 0.02, 0.5, 0.8 ]), 119 | [ 0.25489541679947153, 0.37986184942794204, 0.8289550829657529 ] 120 | ) 121 | } 122 | } 123 | 124 | @include describe('XYZ_to_lin_a98rgb()') { 125 | @include it('convert XYZ to linear-light a98-rgb') { 126 | @include assert-equal( 127 | conversions.XYZ_to_lin_a98rgb([ 0.02, 0.5, 0.8 ]), 128 | [ -0.5174568096858785, 0.951842923953983, 0.7532276850100957 ] 129 | ) 130 | } 131 | } 132 | 133 | @include describe('lin_2020()') { 134 | @include it('convert rec2020 RGB values to linear-light') { 135 | @include assert-equal( 136 | conversions.lin_2020([ 0.02, 0.5, 0.8 ]), 137 | [ 0.0044444444444444444, 0.233165778158119, 0.6175774187196106 ] 138 | ) 139 | } 140 | } 141 | 142 | @include describe('gam_2020()') { 143 | @include it('gamma-correct linear-light rec2020 RGB') { 144 | @include assert-equal( 145 | conversions.gam_2020([ 0.02, 0.5, 0.8 ]), 146 | [ 0.11608586772520829, 0.7242452807888918, 0.9023988565671286 ] 147 | ) 148 | } 149 | } 150 | 151 | @include describe('lin_2020_to_XYZ()') { 152 | @include it('convert linear-light rec2020 values to CIE XYZ') { 153 | @include assert-equal( 154 | conversions.lin_2020_to_XYZ([ 0.02, 0.5, 0.8 ]), 155 | [ 0.22015239289046767, 0.3916944131755503, 0.8628243926931766 ] 156 | ) 157 | } 158 | } 159 | 160 | @include describe('XYZ_to_lin_2020()') { 161 | @include it('convert XYZ to linear-light rec2020') { 162 | @include assert-equal( 163 | conversions.XYZ_to_lin_2020([ 0.02, 0.5, 0.8 ]), 164 | [ -0.34619539322769866, 0.8075217679319489, 0.7326499875083811 ] 165 | ) 166 | } 167 | } 168 | 169 | @include describe('D65_to_D50()') { 170 | @include it('Bradford chromatic adaptation from D65 to D50') { 171 | @include assert-equal( 172 | conversions.D65_to_D50([ 0.02, 0.5, 0.8 ]), 173 | [ -0.007702076000000002, 0.482193768, 0.60904239 ] 174 | ) 175 | } 176 | } 177 | 178 | @include describe('D50_to_D65()') { 179 | @include it('Bradford chromatic adaptation from D50 to D65') { 180 | @include assert-equal( 181 | conversions.D50_to_D65([ 0.02, 0.5, 0.8 ]), 182 | [ 0.058122762, 0.5212111700000001, 1.0539323040000002 ] 183 | ) 184 | } 185 | } 186 | 187 | @include describe('XYZ_to_Lab()') { 188 | @include it('Assuming XYZ is relative to D50, convert to CIE Lab') { 189 | @include assert-equal( 190 | conversions.XYZ_to_Lab([ 0.02, 0.5, 0.8 ]), 191 | [ 76.06926101415557, -259.4709655459667, -39.20214239220301 ] 192 | ) 193 | } 194 | } 195 | 196 | @include describe('Lab_to_XYZ()') { 197 | @include it('Convert Lab to D50-adapted XYZ') { 198 | @include assert-equal( 199 | conversions.Lab_to_XYZ([ 1.02, 0.5, 1.8 ]), 200 | [ 0.0012126186329902817, 0.001129197589077043, -0.00002192529747017181 ] 201 | ) 202 | } 203 | } 204 | 205 | @include describe('Lab_to_LCH()') { 206 | @include it('Convert to polar form') { 207 | @include assert-equal( 208 | conversions.Lab_to_LCH([ 20, 50, 80 ]), 209 | [ 20, 94.33981132056604, 57.9946167919165 ] 210 | ) 211 | } 212 | } 213 | 214 | @include describe('LCH_to_Lab()') { 215 | @include it('Convert from polar form') { 216 | @include assert-equal( 217 | conversions.LCH_to_Lab([ 20, 50, 80 ]), 218 | [ 20, 8.68240888334652, 49.2403876506104 ] 219 | ) 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /test/convert/_extra.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/convert/extra'; 3 | 4 | @include describe('convert/extra') { 5 | @include describe('hueFromColor') { 6 | @include it('Returns a normalized hsl hue from a Sass color') { 7 | @include assert-equal( 8 | extra.hueFromColor(rgb(255, 0, 255)), 9 | 5 10 | ); 11 | } 12 | } 13 | 14 | @include describe('rgbToSass') { 15 | @include it('Converts normalized rgb/a channels to a sass color') { 16 | @include assert-equal( 17 | extra.rgbToSass(1 0 1, 0.5), 18 | rgba(255, 0, 255, 0.5) 19 | ); 20 | } 21 | } 22 | 23 | @include describe('sassToRgb') { 24 | @include it('Extracts normalized rgb values from a Sass color') { 25 | @include assert-equal( 26 | extra.sassToRgb(rgba(255, 0, 255, 0.5)), 27 | (1, 0, 1) 28 | ); 29 | } 30 | } 31 | 32 | @include describe('sassToHsl') { 33 | @include it('Extracts normalized hsl values from a Sass color') { 34 | @include assert-equal( 35 | extra.sassToHsl(rgb(255, 0, 255)), 36 | (5, 1, 0.5) 37 | ); 38 | } 39 | } 40 | 41 | @include describe('rgbToHsl()') { 42 | @include it('Converts rgb to hsl via sass') { 43 | @include assert-equal( 44 | extra.rgbToHsl(1 1 1), 45 | 0 0 1 46 | ); 47 | 48 | @include assert-equal( 49 | extra.rgbToHsl(0 0 0), 50 | 0 0 0 51 | ); 52 | 53 | @include assert-equal( 54 | extra.rgbToHsl(0.5 0.5 0.5), 55 | 0 0 0.5019607843, 56 | $inspect: true 57 | ); 58 | 59 | @include assert-equal( 60 | extra.rgbToHsl(1 0 0), 61 | 0 1 0.5 62 | ); 63 | 64 | @include assert-equal( 65 | extra.rgbToHsl(0.75 0.75 0), 66 | 1 1 0.3745098039, 67 | $inspect: true 68 | ); 69 | 70 | @include assert-equal( 71 | extra.rgbToHsl(0.75 0.25 0.75), 72 | 5 0.4980392157 0.5, 73 | $inspect: true 74 | ); 75 | } 76 | } 77 | 78 | @include describe('rgbToHwb()') { 79 | @include it('Converts normalized srgb values to normalized hwb') { 80 | @include assert-equal( 81 | extra.rgbToHwb(1 0 1), 82 | 5 0 0 83 | ); 84 | } 85 | 86 | @include it('Uses input format as a template') { 87 | @include assert-equal( 88 | extra.rgbToHwb([ 1, 0, 1 ]), 89 | [ 5, 0, 0 ] 90 | ); 91 | } 92 | } 93 | 94 | @include describe('hwbToRgb()') { 95 | @include it('Converts normal hwb channels to srgb') { 96 | @include assert-equal( 97 | extra.hwbToRgb(5 0 0), 98 | 1 0 1 99 | ); 100 | } 101 | } 102 | 103 | @include describe('sassToLCH()') { 104 | @include it('Converts sass colors to LCH format') { 105 | @include assert-equal( 106 | extra.sassToLCH(#484268), 107 | [29.8786112748, 23.6771014858, 294.6544801279], 108 | $inspect: true 109 | ); 110 | } 111 | } 112 | 113 | @include describe('sassToLab()') { 114 | @include it('Converts sass colors to Lab format') { 115 | @include assert-equal( 116 | extra.sassToLab(#484268), 117 | [29.8786112748, 9.8767882407, -21.5186939385], 118 | $inspect: true 119 | ); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/convert/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'conversions'; 2 | @forward 'utilities'; 3 | @forward 'extra'; 4 | -------------------------------------------------------------------------------- /test/convert/_utilities.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/convert/utilities'; 3 | 4 | @include describe('convert/utilities') { 5 | @include describe('sRGB_to_luminance()') { 6 | @include it('converts an array of gamma-corrected sRGB values') { 7 | @include assert-equal( 8 | utilities.sRGB_to_luminance((0.02, 0.5, 0.8)), 9 | 0.1969963759, 10 | $inspect: true 11 | ); 12 | @include assert-equal( 13 | utilities.sRGB_to_luminance((0.8, 0.5, 0.02)), 14 | 0.2815845175, 15 | $inspect: true 16 | ); 17 | } 18 | } 19 | 20 | @include describe('contrast()') { 21 | @include it('returns WCAG 2.1 contrast ratio') { 22 | @include assert-equal( 23 | utilities.contrast((0.02, 0.5, 0.8), (0.8, 0.5, 0.02)), 24 | 1.3424671368, 25 | $inspect: true 26 | ); 27 | @include assert-equal( 28 | utilities.contrast((0, 0, 0), (1, 1, 1)), 29 | 21 30 | ); 31 | } 32 | } 33 | 34 | @include describe('sRGB_to_LCH()') { 35 | @include it('converts an array of gamma-corrected sRGB values to LCH') { 36 | @include assert-equal( 37 | utilities.sRGB_to_LCH((0.02, 0.5, 0.8)), 38 | [50.7340159033, 49.1810731634, 261.1728089306], 39 | $inspect: true 40 | ); 41 | } 42 | } 43 | 44 | @include describe('P3_to_LCH()') { 45 | @include it('converts an array of gamma-corrected display-p3 values to LCH') { 46 | @include assert-equal( 47 | utilities.P3_to_LCH((0.02, 0.5, 0.8)), 48 | [ 50.54052827625061, 54.14426679454487, 256.00726684112874 ] 49 | ); 50 | } 51 | } 52 | 53 | @include describe('r2020_to_LCH()') { 54 | @include it('converts an array of gamma-corrected rec.2020 values to LCH') { 55 | @include assert-equal( 56 | utilities.r2020_to_LCH((0.02, 0.5, 0.8)), 57 | [ 50.32148305473913, 66.1429142189001, 235.98101934236752 ] 58 | ); 59 | } 60 | } 61 | 62 | @include describe('LCH_to_sRGB()') { 63 | @include it('converts an array of CIE LCH values to gamma corrected sRGB') { 64 | @include assert-equal( 65 | utilities.LCH_to_sRGB((0.02, 0.5, 0.8)), 66 | [0.00532342, -0.0012811892, 0.0003339121], 67 | $inspect: true 68 | ); 69 | } 70 | } 71 | 72 | @include describe('LCH_to_P3()') { 73 | @include it('converts an array of CIE LCH values to display-p3') { 74 | @include assert-equal( 75 | utilities.LCH_to_P3((0.02, 0.5, 0.8)), 76 | [ 0.0041508507102366215, -0.001061954523871675, 0.00030221682220373875 ] 77 | ); 78 | } 79 | } 80 | 81 | @include describe('LCH_to_r2020()') { 82 | @include it('converts an array of CIE LCH values to rec.2020') { 83 | @include assert-equal( 84 | utilities.LCH_to_r2020((0.02, 0.5, 0.8)), 85 | [ 0.0010213897095624753, -0.0002808938132111162, 0.00009527562547363652 ] 86 | ); 87 | } 88 | } 89 | 90 | @include describe('hslToRgb()') { 91 | @include it('converts normaized HSL values to RGB') { 92 | @include assert-equal( 93 | utilities.hslToRgb(2.2 0.5 0.8), 94 | 0.7000000000000001 0.9 0.7400000000000001 95 | ); 96 | } 97 | } 98 | 99 | @include describe('hueToRgb()') { 100 | @include it('converts hue to RGB values') { 101 | @include assert-equal( 102 | utilities.hueToRgb(0.7, 0.9, 2.2), 103 | 0.9 104 | ); 105 | @include assert-equal( 106 | utilities.hueToRgb(0.7, 0.9, 0.2), 107 | 0.74 108 | ); 109 | @include assert-equal( 110 | utilities.hueToRgb(0.7, 0.9, 3.2), 111 | 0.86 112 | ); 113 | @include assert-equal( 114 | utilities.hueToRgb(0.7, 0.9, 5.2), 115 | 0.7 116 | ); 117 | } 118 | } 119 | 120 | @include describe('naive_CMYK_to_sRGB()') { 121 | @include it('converts CMYK to RGB using naive math') { 122 | @include assert-equal( 123 | utilities.naive_CMYK_to_sRGB([ 0.2, 0.5, 0.8, 0.12 ]), 124 | [ 0.704, 0.43999999999999995, 0.17599999999999993 ] 125 | ); 126 | } 127 | } 128 | 129 | @include describe('naive_sRGB_to_CMYK()') { 130 | @include it('converts RGB to CMYK using naive math') { 131 | @include assert-equal( 132 | utilities.naive_sRGB_to_CMYK([ 0.2, 0.5, 0.8 ]), 133 | [0.75, 0.375, 0, 0.2] 134 | ); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/index.scss: -------------------------------------------------------------------------------- 1 | @use '../sass/utils/throw' with ($catch-errors: true); 2 | 3 | @use 'utils'; 4 | @use 'convert'; 5 | @use 'cie'; 6 | @use 'inspect'; 7 | @use 'adjust'; 8 | -------------------------------------------------------------------------------- /test/inspect/_cie.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/inspect/cie'; 3 | 4 | @include describe('inspect/cie') { 5 | @include describe('lightness()') { 6 | @include it('Returns the CIE lightness of a Sass color') { 7 | @include assert-equal( 8 | cie.lightness(#672396), 9 | 29.5921856913%, 10 | $inspect: true 11 | ); 12 | } 13 | } 14 | 15 | @include describe('a()') { 16 | @include it('Returns the Lab a channel of a Sass color') { 17 | @include assert-equal( 18 | cie.a(rgb(97,125,108)), 19 | -13.2914711339, 20 | $inspect: true 21 | ); 22 | } 23 | } 24 | 25 | @include describe('b()') { 26 | @include it('Returns the Lab b channel of a Sass color') { 27 | @include assert-equal( 28 | cie.b(rgb(97,125,108)), 29 | 5.8314910038, 30 | $inspect: true 31 | ); 32 | } 33 | } 34 | 35 | @include describe('chroma()') { 36 | @include it('Returns the CIE chroma of a Sass color') { 37 | @include assert-equal( 38 | cie.chroma(#672396), 39 | 67.8432391872, 40 | $inspect: true 41 | ); 42 | } 43 | } 44 | 45 | @include describe('hue()') { 46 | @include it('Returns the CIE hue of a Sass color') { 47 | @include assert-equal( 48 | cie.hue(#672396), 49 | 312.0422966709deg, 50 | $inspect: true 51 | ); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/inspect/_contrast.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/inspect/contrast' as inspect; 3 | 4 | @include describe('inspect/contrast') { 5 | @include describe('contrast()') { 6 | @include it('Selects best conrast between black and white') { 7 | @include assert-equal( 8 | inspect.contrast(maroon), 9 | white 10 | ); 11 | @include assert-equal( 12 | inspect.contrast(yellow), 13 | black 14 | ); 15 | } 16 | 17 | @include it('Selects best conrast between options given') { 18 | @include assert-equal( 19 | inspect.contrast(maroon, orange, yellow, green, blue), 20 | yellow 21 | ); 22 | } 23 | 24 | @include it('Selects first to match a given ratio') { 25 | @include assert-equal( 26 | inspect.contrast(white, orange, yellow, maroon, black, 4.5), 27 | maroon 28 | ); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/inspect/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'contrast'; 2 | @forward 'cie'; 3 | -------------------------------------------------------------------------------- /test/test_sass.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var sassTrue = require('sass-true'); 3 | 4 | var sassFile = path.join(__dirname, '.', 'index.scss'); 5 | sassTrue.runSass({ describe, it }, sassFile); 6 | -------------------------------------------------------------------------------- /test/utils/_array.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/utils/array'; 3 | @use 'sass:list'; 4 | 5 | @function _map( 6 | $e, 7 | $start: 0, 8 | $i: 1, 9 | ) { 10 | $key: ($i + $start); 11 | @return ($key: $e); 12 | } 13 | 14 | $_map: get-function('_map'); 15 | 16 | @function _reducer( 17 | $acc, 18 | $value, 19 | $offset: 0 20 | ) { 21 | $acc: if($acc == null, 1, $acc); 22 | @return $acc * $value + $offset; 23 | } 24 | 25 | $_reducer: get-function('_reducer'); 26 | 27 | @include describe('utils/array') { 28 | @include describe('template()') { 29 | @include it('Returns an empty list of the same type as $array') { 30 | $comma: array.template((1, 2, 3)); 31 | @include assert-equal( 32 | list.length($comma), 33 | 0 34 | ); 35 | @include assert-equal( 36 | list.separator($comma), 37 | 'comma' 38 | ); 39 | @include assert-equal( 40 | list.is-bracketed($comma), 41 | false 42 | ); 43 | 44 | $space: array.template([ 1 2 3 ]); 45 | @include assert-equal( 46 | list.length($space), 47 | 0 48 | ); 49 | @include assert-equal( 50 | list.separator($space), 51 | 'space' 52 | ); 53 | @include assert-equal( 54 | list.is-bracketed($space), 55 | true 56 | ); 57 | } 58 | 59 | @include it('Allows you to override separator') { 60 | @include assert-equal( 61 | list.separator(array.template(())), 62 | 'space' 63 | ); 64 | @include assert-equal( 65 | list.separator(array.template((), 'comma')), 66 | 'comma' 67 | ); 68 | } 69 | 70 | @include it('Allows you to override brackets') { 71 | @include assert-equal( 72 | list.is-bracketed(array.template(())), 73 | false 74 | ); 75 | @include assert-equal( 76 | list.is-bracketed(array.template((), $brackets: true)), 77 | true 78 | ); 79 | } 80 | 81 | @include it('Throws an error when $array is not a list') { 82 | @include assert-equal( 83 | array.template(7), 84 | 'ERROR [array.template()] $array must be one of (list, arglist), got number: 7' 85 | ) 86 | } 87 | } 88 | 89 | @include describe('range()') { 90 | @include it('Generates an array of numbers') { 91 | @include assert-equal( 92 | array.range(1, 5), 93 | (1 2 3 4 5) 94 | ); 95 | } 96 | 97 | @include it('Accepts different steps') { 98 | @include assert-equal( 99 | array.range(1, 10, 2), 100 | (1 3 5 7 9) 101 | ); 102 | } 103 | 104 | @include it('Can exclude the end value') { 105 | @include assert-equal( 106 | array.range(5, 10, $include-end: false), 107 | (5 6 7 8 9) 108 | ); 109 | } 110 | 111 | @include it('Can use any delimiter') { 112 | @include assert-equal( 113 | array.range(5, 10, $separator: comma), 114 | (5, 6, 7, 8, 9, 10) 115 | ); 116 | } 117 | 118 | @include it('Can use brackets') { 119 | @include assert-equal( 120 | array.range(5, 10, $brackets: true), 121 | [5 6 7 8 9 10] 122 | ); 123 | } 124 | } 125 | 126 | @include describe('from-string()') { 127 | @include it('Splits a string into characters') { 128 | @include assert-equal( 129 | array.from-string('abcd', ''), 130 | ('a' 'b' 'c' 'd') 131 | ); 132 | } 133 | @include it('Splits a string from separator') { 134 | @include assert-equal( 135 | array.from-string('ab, cd', ', '), 136 | ('ab' 'cd') 137 | ); 138 | } 139 | } 140 | 141 | @include describe('slice()') { 142 | $animals: ['ant', 'bison', 'camel', 'duck', 'elephant']; 143 | 144 | @include it('Slices from after the beginning index') { 145 | @include assert-equal( 146 | array.slice($animals, 2), 147 | ['camel', 'duck', 'elephant'] 148 | ); 149 | } 150 | 151 | @include it('Slices from beginning through end') { 152 | @include assert-equal( 153 | array.slice($animals, 2, 4), 154 | ['camel', 'duck'] 155 | ); 156 | } 157 | 158 | @include it('Handles negative-index for end') { 159 | @include assert-equal( 160 | array.slice($animals, 2, -1), 161 | ['camel', 'duck'] 162 | ); 163 | } 164 | 165 | @include it('Handles extra-large end index') { 166 | @include assert-equal( 167 | array.slice($animals, 2, 10), 168 | ['camel', 'duck', 'elephant'] 169 | ); 170 | } 171 | } 172 | 173 | @include describe('join()') { 174 | $array: (Miriam 37 Suzanne); 175 | 176 | @include it('Merges array values into a string') { 177 | @include assert-equal( 178 | array.join($array), 179 | 'Miriam37Suzanne' 180 | ); 181 | } 182 | 183 | @include it('Accepts a delimiter') { 184 | @include assert-equal( 185 | array.join($array, '::'), 186 | 'Miriam::37::Suzanne' 187 | ); 188 | } 189 | 190 | @include it('Throws an error when $array is not a list') { 191 | @include assert-equal( 192 | array.join(7), 193 | 'ERROR [array.join()] $array must be a list, got number: 7' 194 | ) 195 | } 196 | } 197 | 198 | @include describe('map()') { 199 | $array: [1 2 3 4 5]; 200 | 201 | @include it('Runs array items through a function') { 202 | @include assert-equal( 203 | array.map($array, $_map), 204 | [ (1: 1) (1: 2) (1: 3) (1: 4) (1: 5) ] 205 | ); 206 | } 207 | 208 | @include it('Accepts arbitrary arguments') { 209 | @include assert-equal( 210 | array.map($array, $_map, 2, 3), 211 | [ (5: 1) (5: 2) (5: 3) (5: 4) (5: 5) ] 212 | ); 213 | } 214 | 215 | @include it('Replaces first instance of `i` or `index` with array index') { 216 | @include assert-equal( 217 | array.map($array, $_map, 2, i), 218 | [ (3: 1) (4: 2) (5: 3) (6: 4) (7: 5) ] 219 | ); 220 | @include assert-equal( 221 | array.map($array, $_map, 2, 'index'), 222 | [ (3: 1) (4: 2) (5: 3) (6: 4) (7: 5) ] 223 | ); 224 | } 225 | 226 | @include it('Throws an error when $array is not a list') { 227 | @include assert-equal( 228 | array.map(7, $_map), 229 | 'ERROR [array.map()] $array must be a list, got number: 7' 230 | ) 231 | } 232 | 233 | @include it('Throws an error when $array is not a list') { 234 | @include assert-equal( 235 | array.map(1 2 3, 7), 236 | 'ERROR [array.map()] $function must be a function, got number: 7' 237 | ) 238 | } 239 | } 240 | 241 | @include describe('reduce()') { 242 | $array: 2 4 6; 243 | @include it('Combines array items through a reducer function') { 244 | @include assert-equal( 245 | array.reduce($array, $_reducer), 246 | 48 247 | ); 248 | } 249 | 250 | @include it('Accepts an initial value') { 251 | @include assert-equal( 252 | array.reduce($array, $_reducer, 2), 253 | 96 254 | ); 255 | } 256 | 257 | @include it('Accepts arbitrary arguments') { 258 | @include assert-equal( 259 | array.reduce($array, $_reducer, 2, 1), 260 | 127 261 | ); 262 | } 263 | 264 | @include it('Accepts array index') { 265 | @include assert-equal( 266 | array.reduce($array, $_reducer, 2, i), 267 | 135 268 | ); 269 | } 270 | 271 | @include it('Throws an error when $array is not a list') { 272 | @include assert-equal( 273 | array.reduce(7, $_reducer), 274 | 'ERROR [array.reduce()] $array must be a list, got number: 7' 275 | ) 276 | } 277 | 278 | @include it('Throws an error when $array is not a list') { 279 | @include assert-equal( 280 | array.reduce(1 2 3, 7), 281 | 'ERROR [array.reduce()] $function must be a function, got number: 7' 282 | ) 283 | } 284 | } 285 | 286 | @include describe('add()') { 287 | @include it('Adds two numbers') { 288 | @include assert-equal( 289 | array.add(2, 4), 290 | 6 291 | ); 292 | } 293 | @include it('Adds equal-length lists') { 294 | @include assert-equal( 295 | array.add((2 4), (6, 8)), 296 | (8 12) 297 | ); 298 | } 299 | @include it('Adds a number to each item in a list') { 300 | @include assert-equal( 301 | array.add((2 4 6), 6), 302 | (8 10 12) 303 | ); 304 | } 305 | } 306 | 307 | @include describe('sum()') { 308 | @include it('Returns the sum of numbers in a list') { 309 | @include assert-equal( 310 | array.sum(2 4 6 8), 311 | 20 312 | ); 313 | } 314 | 315 | @include it('Returns the sum of numbers and lists') { 316 | @include assert-equal( 317 | array.sum(2 (4 6) 8), 318 | (14 16) 319 | ); 320 | } 321 | 322 | @include it('Returns the sum of sub-lists') { 323 | @include assert-equal( 324 | array.sum((2 4) (6 8)), 325 | (8 12) 326 | ); 327 | } 328 | } 329 | 330 | @include describe('product()') { 331 | @include it('Returns the product of numbers in a list') { 332 | @include assert-equal( 333 | array.product(2 4 6 8), 334 | 384 335 | ); 336 | } 337 | 338 | @include it('Returns the product of numbers and lists') { 339 | @include assert-equal( 340 | array.product(2 (4 6) 8), 341 | (64 96) 342 | ); 343 | } 344 | 345 | @include it('Returns the product of sub-lists') { 346 | @include assert-equal( 347 | array.product((2 4) (6 8)), 348 | (12 32) 349 | ); 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /test/utils/_channel.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/utils/channel'; 3 | 4 | @include describe('utils/channel') { 5 | @include describe('deg6') { 6 | @include it('Converts degrees to a half-open range [0,6)') { 7 | @include assert-equal( 8 | channel.deg6(3), 9 | 0.05 10 | ); 11 | @include assert-equal( 12 | channel.deg6(180), 13 | 3 14 | ); 15 | } 16 | 17 | @include it('Range is half-open') { 18 | @include assert-equal( 19 | channel.deg6(0), 20 | 0 21 | ); 22 | @include assert-equal( 23 | channel.deg6(360), 24 | 0 25 | ); 26 | } 27 | 28 | @include it('Handles out-of-range numbers') { 29 | @include assert-equal( 30 | channel.deg6(-20), 31 | channel.deg6(340) 32 | ); 33 | @include assert-equal( 34 | channel.deg6(700), 35 | channel.deg6(340) 36 | ); 37 | } 38 | } 39 | 40 | @include describe('fraction()') { 41 | @include it('generates a fraction') { 42 | @include assert-equal( 43 | channel.fraction(30), 44 | 0.3 45 | ); 46 | } 47 | 48 | @include it('generates a fraction based on a range') { 49 | @include assert-equal( 50 | channel.fraction(30, 300), 51 | 0.1 52 | ); 53 | } 54 | } 55 | 56 | @include describe('fractions()') { 57 | @include it('generates channel fractions') { 58 | @include assert-equal( 59 | channel.fractions(30 45 82), 60 | 0.3 0.45 0.82 61 | ); 62 | } 63 | 64 | @include it('generates achannel fractions based on a range') { 65 | @include assert-equal( 66 | channel.fractions(30 60 90, 300), 67 | 0.1 0.2 0.3 68 | ); 69 | } 70 | } 71 | 72 | @include describe('format()') { 73 | @include it('Formats the proper values/units for rgb') { 74 | @include assert-equal( 75 | channel.format(0.25 1 0.5432101), 76 | 25% 100% 54.32% 77 | ); 78 | } 79 | 80 | @include it('Can generate hsl format') { 81 | @include assert-equal( 82 | channel.format(5 1 0.5432101, 60deg 100% 100%), 83 | 300deg 100% 54.32% 84 | ); 85 | } 86 | 87 | @include it('Can generate color() formats') { 88 | @include assert-equal( 89 | channel.format(0.1234321 1 0.5432101, 1, 4), 90 | 0.1234 1 0.5432 91 | ); 92 | } 93 | } 94 | 95 | @include describe('valid()') { 96 | @include it('Returns a valid list of channels') { 97 | @include assert-equal( 98 | channel.valid(30 45 2, 'test', 'rgb'), 99 | 30 45 2 100 | ); 101 | } 102 | 103 | @include it('Allows different channel-count') { 104 | @include assert-equal( 105 | channel.valid(30 45 2 24, 'test', 'rgb', 4), 106 | 30 45 2 24 107 | ); 108 | } 109 | 110 | @include it('Throws when not a list') { 111 | @include assert-equal( 112 | channel.valid('string', 'test', 'rgb'), 113 | 'ERROR [test] $rgb must be a list, got string: string' 114 | ); 115 | } 116 | 117 | @include it('Throws when wrong length list') { 118 | @include assert-equal( 119 | channel.valid(30 42, 'test', 'rgb'), 120 | 'ERROR [test] $rgb expects 3 channels, got 2' 121 | ); 122 | } 123 | 124 | @include it('Throws when channels are not numbers') { 125 | @include assert-equal( 126 | channel.valid(30 42 'red', 'test', 'rgb'), 127 | 'ERROR [test] $rgb channels must be a number, got string: red' 128 | ); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/utils/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'array'; 2 | @forward 'channel'; 3 | @forward 'matrix'; 4 | @forward 'pow'; 5 | @forward 'relative'; 6 | @forward 'units'; 7 | -------------------------------------------------------------------------------- /test/utils/_matrix.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/utils/array'; 3 | @use '../../sass/utils/matrix'; 4 | @use 'sass:list'; 5 | 6 | @include describe('utils/matrix') { 7 | @include describe('is-2d()') { 8 | @include it('Returns true when there are one or more sub-row') { 9 | @include assert-equal( 10 | matrix.is-2d([ [ 1 2 3 ] [ 4 5 6 ] ]), 11 | true 12 | ); 13 | @include assert-equal( 14 | matrix.is-2d([ [ 1 2 3 ] ]), 15 | true 16 | ); 17 | } 18 | 19 | @include it('Returns false when a single row is not nested') { 20 | @include assert-equal( 21 | matrix.is-2d([ 4 5 6 ]), 22 | false 23 | ); 24 | } 25 | } 26 | 27 | @include describe('force-2d()') { 28 | @include it('Returns a 2d matrix without changes') { 29 | @include assert-equal( 30 | matrix.force-2d([ [ 1 2 3 ] ]), 31 | [ [ 1 2 3 ] ] 32 | ); 33 | } 34 | 35 | @include it('Converts a 1d matrix into 2d') { 36 | @include assert-equal( 37 | matrix.force-2d([ 4 5 6 ]), 38 | list.append([], [ 4 5 6 ], space) 39 | ); 40 | } 41 | } 42 | 43 | @include describe('is-valid') { 44 | $type-error: 'Miriam'; 45 | $sub-error: [ 2 4 'Miriam']; 46 | $length-error: [ 47 | [ 1 2 3 ] 48 | [ 4 5 ] 49 | ]; 50 | $valid: [ 51 | [ 1 2 3 ] 52 | [ 4 5 6 ] 53 | ]; 54 | 55 | @include it('Returns the matrix if valid'){ 56 | @include assert-equal( 57 | matrix.is-valid($valid), 58 | $valid 59 | ); 60 | 61 | $simple: list.nth($valid, 1); 62 | @include assert-equal( 63 | matrix.is-valid($simple), 64 | $simple 65 | ); 66 | 67 | @include assert-equal( 68 | matrix.is-valid([ ]), 69 | [ ] 70 | ); 71 | } 72 | 73 | @include it('Throws on wrong type'){ 74 | @include assert-equal( 75 | matrix.is-valid($type-error), 76 | 'ERROR [matrix.is-valid()] $matrix must be a list, got string: Miriam' 77 | ); 78 | } 79 | 80 | @include it('Throws on wrong item types'){ 81 | @include assert-equal( 82 | matrix.is-valid($sub-error), 83 | 'ERROR [matrix.is-valid()] Matrix items must be numbers, or equal-length lists of numbers' 84 | ); 85 | } 86 | 87 | @include it('Throws on unequal rows'){ 88 | @include assert-equal( 89 | matrix.is-valid($length-error), 90 | 'ERROR [matrix.is-valid()] Matrix items must be numbers, or equal-length lists of numbers' 91 | ); 92 | } 93 | } 94 | 95 | @include describe('is()') { 96 | @include it('Recognizes a scalar') { 97 | @include assert-equal( 98 | matrix.is(2.3), 99 | 'scalar' 100 | ); 101 | } 102 | @include it('Checks a scalar') { 103 | @include assert-true(matrix.is(2.3, 'scalar')); 104 | @include assert-false(matrix.is(2.3, 'matrix')); 105 | @include assert-false(matrix.is(2.3, 'vector')); 106 | } 107 | 108 | @include it('Recognizes a matrix') { 109 | @include assert-equal( 110 | matrix.is([ (1 2 3) ]), 111 | 'matrix' 112 | ); 113 | } 114 | @include it('Checks a matrix') { 115 | @include assert-false(matrix.is([ (1 2 3) ], 'scalar')); 116 | @include assert-true(matrix.is([ (1 2 3) (4, 5, 6) ], 'matrix')); 117 | @include assert-false(matrix.is([ (1 2 3) ], 'vector')); 118 | } 119 | 120 | @include it('Recognizes a vector') { 121 | @include assert-equal( 122 | matrix.is((1 2 3)), 123 | 'vector' 124 | ); 125 | } 126 | @include it('Checks a vector') { 127 | @include assert-false(matrix.is((1 2 3), 'scalar')); 128 | @include assert-false(matrix.is((1 2 3), 'matrix')); 129 | @include assert-true(matrix.is((1 2 3), 'vector')); 130 | } 131 | } 132 | 133 | @include describe('size()') { 134 | @include it('Returns an empty list for scalars') { 135 | @include assert-equal( 136 | matrix.size(2.3), 137 | [] 138 | ); 139 | } 140 | 141 | @include it('Returns zero columns/rows for scalars') { 142 | @include assert-equal( 143 | matrix.size(2.3, 'columns'), 144 | 0 145 | ); 146 | @include assert-equal( 147 | matrix.size(2.3, 'rows'), 148 | 0 149 | ); 150 | } 151 | 152 | @include it('Returns single-dimension of a vector') { 153 | @include assert-equal( 154 | matrix.size([ ]), 155 | [0] 156 | ); 157 | @include assert-equal( 158 | matrix.size([ 1 2 3 ]), 159 | [3] 160 | ); 161 | } 162 | 163 | @include it('Returns columns of a vector') { 164 | @include assert-equal( 165 | matrix.size([ ], 'columns'), 166 | 0 167 | ); 168 | @include assert-equal( 169 | matrix.size([ 1 2 3 ], 'columns'), 170 | 3 171 | ); 172 | } 173 | 174 | @include it('Returns 0 rows of a vector') { 175 | @include assert-equal( 176 | matrix.size([ ], 'rows'), 177 | 0 178 | ); 179 | @include assert-equal( 180 | matrix.size([ 1 2 3 ], 'rows'), 181 | 0 182 | ); 183 | } 184 | 185 | @include it('Returns an array of column & row lengths') { 186 | @include assert-equal( 187 | matrix.size([ [ 1 2 3 ] [ 4 5 6 ] ]), 188 | [2, 3] 189 | ); 190 | } 191 | 192 | @include it('Optionally returns column-count') { 193 | @include assert-equal( 194 | matrix.size([ [ 1 2 3 ] [ 4 5 6 ] ], 'columns'), 195 | 3 196 | ); 197 | } 198 | 199 | @include it('Optionally returns row-count') { 200 | @include assert-equal( 201 | matrix.size([ [ 1 2 3 ] [ 4 5 6 ] ], 'rows'), 202 | 2 203 | ); 204 | } 205 | } 206 | 207 | @include describe('squeeze()') { 208 | @include it('Returns a scalar unchanged') { 209 | @include assert-equal( 210 | matrix.squeeze(2.3), 211 | 2.3 212 | ); 213 | } 214 | 215 | @include it('Returns a vector unchanged') { 216 | @include assert-equal( 217 | matrix.squeeze([ 1, 2, 3 ]), 218 | [ 1, 2, 3 ] 219 | ); 220 | } 221 | 222 | @include it('Converts a single-row matrix to a vector') { 223 | @include assert-equal( 224 | matrix.squeeze(([ 1, 2, 3 ])), 225 | [ 1, 2, 3 ] 226 | ); 227 | } 228 | 229 | @include it('Works recursively') { 230 | @include assert-unequal( 231 | ([([ 1, 2, 3 ])]), 232 | [ 1, 2, 3 ] 233 | ); 234 | @include assert-equal( 235 | matrix.squeeze(([([ 1, 2, 3 ])])), 236 | [ 1, 2, 3 ] 237 | ); 238 | } 239 | } 240 | 241 | @include describe('transpose()') { 242 | $space: array.template(( 1 2 )); 243 | $comma: array.template(( 1, 2 )); 244 | $b-space: array.template([ 1 2 ]); 245 | $b-comma: array.template([ 1, 2 ]); 246 | 247 | @include it('Transposes a single array') { 248 | @include assert-equal( 249 | matrix.transpose([ 1 2 3 ]), 250 | [ 251 | list.append($b-space, 1) 252 | list.append($b-space, 2) 253 | list.append($b-space, 3) 254 | ] 255 | ); 256 | } 257 | 258 | @include it('Transposes a single row') { 259 | @include assert-equal( 260 | matrix.transpose([ [ 1, 2, 3 ] ]), 261 | [ 262 | list.append($b-comma, 1) 263 | list.append($b-comma, 2) 264 | list.append($b-comma, 3) 265 | ] 266 | ); 267 | } 268 | 269 | @include it('Transposes a single column') { 270 | @include assert-equal( 271 | matrix.transpose(( [ 1 ], [ 2 ], [ 3 ] )), 272 | list.append($comma, [ 1 2 3 ] ) 273 | ); 274 | } 275 | 276 | @include it('Transposes a columns & rows') { 277 | @include assert-equal( 278 | matrix.transpose([ ( 1, 2, 3 ), ( 4, 5, 6 ), ( 7, 8, 9 ) ]), 279 | [ 280 | ( 1, 4, 7 ), 281 | ( 2, 5, 8 ), 282 | ( 3, 6, 9 ), 283 | ] 284 | ); 285 | } 286 | } 287 | 288 | @include describe('get()') { 289 | @include it('Returns nth-index of a vactor') { 290 | @include assert-equal( 291 | matrix.get([ 2 4 6 8 ], 2), 292 | 4 293 | ); 294 | } 295 | 296 | @include it('Returns multi-dimensional index of a matrix') { 297 | @include assert-equal( 298 | matrix.get([ [ 2 4 10 ] [ 6 8 12 ] [ 1 3 5 ] ], [ 2, 2 ]), 299 | 8 300 | ); 301 | } 302 | } 303 | 304 | @include describe('set()') { 305 | @include it('Sets nth-index of a vactor') { 306 | @include assert-equal( 307 | matrix.set([ 2 4 6 8 ], 2, 12), 308 | [ 2 12 6 8 ] 309 | ); 310 | } 311 | 312 | @include it('Sets multi-dimensional index of a matrix') { 313 | @include assert-equal( 314 | matrix.set([ [ 2 4 10 ] [ 6 8 12 ] [ 1 3 5 ] ], [ 2, 2 ], 12), 315 | [ [ 2 4 10 ] [ 6 12 12 ] [ 1 3 5 ] ] 316 | ); 317 | } 318 | } 319 | 320 | @include describe('multiply()') { 321 | $c: [[1, 2], [4, 3]]; 322 | $d: [[1, 2, 3], [3, -4, 7]]; 323 | 324 | @include it('Multiplies a matrix with a scalar') { 325 | @include assert-equal( 326 | matrix.multiply($c, 2), 327 | [[2, 4], [8, 6]] 328 | ); 329 | @include assert-equal( 330 | matrix.multiply($d, -2), 331 | [[-2, -4, -6], [-6, 8, -14]] 332 | ); 333 | } 334 | 335 | @include it('Multiplies two matricies') { 336 | @include assert-equal( 337 | matrix.multiply($c, $d), 338 | [[7, -6, 17], [13, -4, 33]] 339 | ); 340 | } 341 | 342 | @include it('Multiplies a vector by a matrix') { 343 | @include assert-equal( 344 | matrix.multiply((2 1 0), [[ 1, 2 ], [ 4, 3 ], [ 5, 5 ]]), 345 | (6 7) 346 | ); 347 | } 348 | 349 | @include it('Multiplies a matrix by a vector') { 350 | @include assert-equal( 351 | matrix.multiply($d, [ 2, 1, 0 ]), 352 | [ 4, 2 ] 353 | ); 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /test/utils/_pow.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/utils/pow'; 3 | 4 | @include describe('utils/pow') { 5 | @include describe('cbrt()') { 6 | @include it('Returns the cubic root of a number'){ 7 | @include assert-equal( 8 | pow.cbrt(27), 9 | 3 10 | ); 11 | @include assert-equal( 12 | pow.cbrt(-64), 13 | -4 14 | ); 15 | } 16 | 17 | @include it('Returns the cubic root of items in an array'){ 18 | @include assert-equal( 19 | pow.cbrt([27, 64, 125]), 20 | [3, 4, 5] 21 | ); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/utils/_relative.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/utils/relative'; 3 | 4 | @include describe('utils/relative') { 5 | @include describe('compile()') { 6 | @include it('Replaces values from source'){ 7 | @include assert-equal( 8 | relative.compile(20 c 80, 10 30 50, lch), 9 | 20 30 80 10 | ); 11 | @include assert-equal( 12 | relative.compile(20 l 80, 10 30 50, lch), 13 | 20 10 80 14 | ); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/utils/_units.scss: -------------------------------------------------------------------------------- 1 | @use '../../node_modules/sass-true/sass/true' as *; 2 | @use '../../sass/utils/units'; 3 | 4 | @include describe('utils/units') { 5 | @include describe('strip()') { 6 | @include it('Returns a unitless number'){ 7 | @include assert-equal( 8 | units.strip(27em), 9 | 27 10 | ); 11 | @include assert-equal( 12 | units.strip(64%), 13 | 64 14 | ); 15 | @include assert-equal( 16 | units.strip(64), 17 | 64 18 | ); 19 | } 20 | } 21 | 22 | @include describe('strip-all()') { 23 | @include it('Strips units from every number in a list') { 24 | @include assert-equal( 25 | units.strip-all(27em 30% 2px 64), 26 | 27 30 2 64 27 | ); 28 | } 29 | } 30 | 31 | @include describe('to-degrees()') { 32 | @include it('Converts a unitless number to degrees') { 33 | @include assert-equal( 34 | units.to-degrees(40), 35 | 40deg 36 | ); 37 | } 38 | 39 | @include it('Converts turns to degrees') { 40 | @include assert-equal( 41 | units.to-degrees(0.5turn), 42 | 180deg 43 | ); 44 | } 45 | 46 | @include it('Converts radians to degrees') { 47 | @include assert-equal( 48 | units.to-degrees(1.570796rad), 49 | 89.999981276deg, 50 | $inspect: true 51 | ); 52 | } 53 | } 54 | } 55 | --------------------------------------------------------------------------------