Initial commit
This commit is contained in:
commit
bb2c7e27df
5 changed files with 420 additions and 0 deletions
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# npm
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Windows image file caches
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Operating System Files
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must ends with two \r.
|
||||||
|
Icon
|
||||||
|
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear on external disk
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
22
README.md
Normal file
22
README.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# postcss-color-contrast-ratio
|
||||||
|
|
||||||
|
[a]: https://postcss.org
|
||||||
|
[b]: https://www.w3.org/TR/WCAG/#contrast-minimum
|
||||||
|
|
||||||
|
[PostCSS][a] plugin which allows for creation of colors based upon [WCAG contrast ratios][b].
|
||||||
|
|
||||||
|
In:
|
||||||
|
```css
|
||||||
|
body {
|
||||||
|
color: crab(5, 0, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Out:
|
||||||
|
```css
|
||||||
|
body {
|
||||||
|
color: #717171;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This readme is a stub. It will contain more information in the future.
|
233
index.js
Normal file
233
index.js
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
const postcss = require('postcss');
|
||||||
|
const parser = require('postcss-values-parser');
|
||||||
|
const colorString = require('color-string');
|
||||||
|
|
||||||
|
module.exports = postcss.plugin('postcss-color-contrast-ratio', () => {
|
||||||
|
/* Yes there are color libraries for JavaScript, but haven't found any that does
|
||||||
|
color conversion from CIELAB to RGB correctly.
|
||||||
|
|
||||||
|
CIELAB and CIEXYZ need to use the D50 standard illuminant per the CSS color
|
||||||
|
specification (and because Photoshop uses it, too), and most libs will use
|
||||||
|
the illuminant of the input color. In the case of sRGB that'd be D65, and that's
|
||||||
|
incorrect. */
|
||||||
|
|
||||||
|
const lab2xyz = (() => {
|
||||||
|
const D50 = [ 0.96422, 1, 0.82521 ];
|
||||||
|
|
||||||
|
function f(t) {
|
||||||
|
return (t > 6 / 29) ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function lab2xyz([ l, a, b ]) {
|
||||||
|
const fl = (l + 16) / 116;
|
||||||
|
const fa = a / 500;
|
||||||
|
const fb = b / 200;
|
||||||
|
|
||||||
|
return [
|
||||||
|
D50[0] * f(fl + fa),
|
||||||
|
D50[1] * f(fl),
|
||||||
|
D50[2] * f(fl - fb)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const xyz2rgb = (() => {
|
||||||
|
function sRGBCompanding(v) {
|
||||||
|
return (v <= 0.0031308) ? 12.92 * v : 1.055 * (v ** (1 / 2.4)) - 0.055;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion matrix from D50 to D65. Normally you'd go through a chromatic
|
||||||
|
// adaptation algorithm here then convert to RGB, but I pulled the result matrix
|
||||||
|
// from http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html instead.
|
||||||
|
const sRGBD50toD65Matrix = [
|
||||||
|
3.1338561, -1.6168667, -0.4906146,
|
||||||
|
-0.9787684, 1.9161415, 0.0334540,
|
||||||
|
0.0719453, -0.2289914, 1.4052427
|
||||||
|
];
|
||||||
|
|
||||||
|
return function xyz2rgb([ x, y, z ]) {
|
||||||
|
let lrgb = matrixMultiplyVector(sRGBD50toD65Matrix, [x, y, z]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
Math.min(Math.max(0, sRGBCompanding(lrgb[0])), 1),
|
||||||
|
Math.min(Math.max(0, sRGBCompanding(lrgb[1])), 1),
|
||||||
|
Math.min(Math.max(0, sRGBCompanding(lrgb[2])), 1)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const matrixMultiplyVector = ( [ a, b, c, d, e, f, g, h, i ], [x, y, z]) => {
|
||||||
|
return [
|
||||||
|
a * x + b * y + c * z,
|
||||||
|
d * x + e * y + f * z,
|
||||||
|
g * x + h * y + i * z
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const relativeLuminance = ([ r, g, b ]) => {
|
||||||
|
if (r <= 0.03928) {
|
||||||
|
r = r / 12.92;
|
||||||
|
} else {
|
||||||
|
r = Math.pow(((r + 0.055) / 1.055), 2.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g <= 0.03928) {
|
||||||
|
g = g / 12.92;
|
||||||
|
} else {
|
||||||
|
g = Math.pow(((g + 0.055) / 1.055), 2.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b <= 0.03928) {
|
||||||
|
b = b / 12.92;
|
||||||
|
} else {
|
||||||
|
b = Math.pow(((b + 0.055) / 1.055), 2.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RGBtoHex = ([ r, g, b ]) => {
|
||||||
|
r = Math.round(r * 255).toString(16);
|
||||||
|
g = Math.round(g * 255).toString(16);
|
||||||
|
b = Math.round(b * 255).toString(16);
|
||||||
|
|
||||||
|
if (r.length % 2) {
|
||||||
|
r = '0' + r;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g.length % 2) {
|
||||||
|
g = '0' + g;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.length % 2) {
|
||||||
|
b = '0' + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `#${r}${g}${b}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseCrab = (node) => {
|
||||||
|
let check = '',
|
||||||
|
fail = false,
|
||||||
|
// Weird way to handle argument checking, but bear with me...
|
||||||
|
arguments = node.nodes.filter((n) => {
|
||||||
|
// The css value parser includes the parentheses and commas, so filter them out
|
||||||
|
if (n.type === 'paren' || n.type === 'comma') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// While doing so add the types to a string to be checked momentarily
|
||||||
|
check += n.type;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the arguments length isn't 3 or 4 then they're invalid.
|
||||||
|
// If the arguments aren't one of the following then they're invalid:
|
||||||
|
// number, number, number
|
||||||
|
// number, number, number, word
|
||||||
|
// number, number, number, function
|
||||||
|
if ((arguments.length === 3 || arguments.length === 4) && (check === 'numbernumbernumber' || check === 'numbernumbernumberword' || check === 'numbernumbernumberfunc')) {
|
||||||
|
let cr = arguments[0].value,
|
||||||
|
a = arguments[1].value,
|
||||||
|
b = arguments[2].value,
|
||||||
|
bg = [ 1, 1, 1 ];
|
||||||
|
|
||||||
|
// Normalize the arguments
|
||||||
|
// WCAG contrast ratio is a value from 1 to 21; starting at 1.4 here
|
||||||
|
cr = Math.min(Math.max(cr, 1.4), 21);
|
||||||
|
// a* is a value from -128 to 127
|
||||||
|
a = Math.min(Math.max(a, -128), 127);
|
||||||
|
// b* is a value from -128 to 127
|
||||||
|
b = Math.min(Math.max(b, -128), 127);
|
||||||
|
|
||||||
|
// The last argument isn't required, so if it is defined then parse it
|
||||||
|
if (arguments[3] !== undefined) {
|
||||||
|
bg = arguments[3];
|
||||||
|
|
||||||
|
// If it is another crab function then parse it recursively
|
||||||
|
if (bg.type ==='func' && bg.value === 'crab') {
|
||||||
|
bg = parseCrab(bg);
|
||||||
|
|
||||||
|
if (bg === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise, if it is a color constant then try to parse it
|
||||||
|
// Otherwise, if it's some color function parse that, too
|
||||||
|
else {
|
||||||
|
bg = (bg.type === 'word') ? bg.value : bg.toString().trim();
|
||||||
|
|
||||||
|
if (bg === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the values to binary notation
|
||||||
|
bg = colorString.get.rgb(bg).slice(0, 3).map((n) => {
|
||||||
|
return n / 255;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any of the parsers failed then return null
|
||||||
|
if (bg === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Optimize
|
||||||
|
// Start from 100 L*a*b luminance and go backwards until the color is within the
|
||||||
|
// desired contrast ratio range and return it
|
||||||
|
|
||||||
|
let l = 100,
|
||||||
|
bgLum = relativeLuminance(bg),
|
||||||
|
c, cLum, ratio;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
c = lab2xyz([ l, a, b ]);
|
||||||
|
c = xyz2rgb(c);
|
||||||
|
|
||||||
|
cLum = relativeLuminance(c);
|
||||||
|
|
||||||
|
ratio = (bgLum + 0.05) / (cLum + 0.05);
|
||||||
|
|
||||||
|
if (cLum > bgLum) {
|
||||||
|
ratio = 1 / ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ratio - 0.1 <= cr && ratio + 0.1 >= cr) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
l -= 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (css) => {
|
||||||
|
css.walkDecls((decl) => {
|
||||||
|
// At first at least check if the value might have a crab function
|
||||||
|
if (!/(^|[^\w-])crab\(/i.test(decl.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it might then parse the values and walk through them
|
||||||
|
let parsedValue = parser(decl.value).parse();
|
||||||
|
parsedValue.walk((node) => {
|
||||||
|
if (node.type === 'func' && node.value === 'crab') {
|
||||||
|
const value = parseCrab(node);
|
||||||
|
|
||||||
|
node.removeAll();
|
||||||
|
node.value = (value !== null) ? RGBtoHex(value) : 'transparent';
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
decl.value = parsedValue.toString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
14
package.json
Normal file
14
package.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "postcss-color-contrast-ratio",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "PostCSS plugin which allows for creation of colors based upon WCAG contrast ratios",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": "https://code.mensbeam.com/MensBeam/postcss-color-contrast-ratio.git",
|
||||||
|
"author": "Dustin Wilson <dustin@dustinwilson.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-string": "^1.5.3",
|
||||||
|
"postcss": "^7.0.7",
|
||||||
|
"postcss-values-parser": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
111
yarn.lock
Normal file
111
yarn.lock
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
ansi-styles@^3.2.1:
|
||||||
|
version "3.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||||
|
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
|
||||||
|
dependencies:
|
||||||
|
color-convert "^1.9.0"
|
||||||
|
|
||||||
|
chalk@^2.4.1:
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
|
||||||
|
integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^3.2.1"
|
||||||
|
escape-string-regexp "^1.0.5"
|
||||||
|
supports-color "^5.3.0"
|
||||||
|
|
||||||
|
color-convert@^1.9.0:
|
||||||
|
version "1.9.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
|
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||||
|
dependencies:
|
||||||
|
color-name "1.1.3"
|
||||||
|
|
||||||
|
color-name@1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||||
|
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||||
|
|
||||||
|
color-name@^1.0.0:
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
|
color-string@^1.5.3:
|
||||||
|
version "1.5.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
|
||||||
|
integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
|
||||||
|
dependencies:
|
||||||
|
color-name "^1.0.0"
|
||||||
|
simple-swizzle "^0.2.2"
|
||||||
|
|
||||||
|
escape-string-regexp@^1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||||
|
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||||
|
|
||||||
|
flatten@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
|
||||||
|
integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=
|
||||||
|
|
||||||
|
has-flag@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||||
|
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
|
||||||
|
|
||||||
|
indexes-of@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
|
||||||
|
integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
|
||||||
|
|
||||||
|
is-arrayish@^0.3.1:
|
||||||
|
version "0.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||||
|
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
||||||
|
|
||||||
|
postcss-values-parser@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.0.tgz#1ba42cae31367c44f96721cb5eb99462bfb39705"
|
||||||
|
integrity sha512-cyRdkgbRRefu91ByAlJow4y9w/hnBmmWgLpWmlFQ2bpIy2eKrqowt3VeYcaHQ08otVXmC9V2JtYW1Z/RpvYR8A==
|
||||||
|
dependencies:
|
||||||
|
flatten "^1.0.2"
|
||||||
|
indexes-of "^1.0.1"
|
||||||
|
uniq "^1.0.1"
|
||||||
|
|
||||||
|
postcss@^7.0.7:
|
||||||
|
version "7.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.7.tgz#2754d073f77acb4ef08f1235c36c5721a7201614"
|
||||||
|
integrity sha512-HThWSJEPkupqew2fnuQMEI2YcTj/8gMV3n80cMdJsKxfIh5tHf7nM5JigNX6LxVMqo6zkgQNAI88hyFvBk41Pg==
|
||||||
|
dependencies:
|
||||||
|
chalk "^2.4.1"
|
||||||
|
source-map "^0.6.1"
|
||||||
|
supports-color "^5.5.0"
|
||||||
|
|
||||||
|
simple-swizzle@^0.2.2:
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
|
||||||
|
integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
|
||||||
|
dependencies:
|
||||||
|
is-arrayish "^0.3.1"
|
||||||
|
|
||||||
|
source-map@^0.6.1:
|
||||||
|
version "0.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||||
|
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||||
|
|
||||||
|
supports-color@^5.3.0, supports-color@^5.5.0:
|
||||||
|
version "5.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||||
|
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||||
|
dependencies:
|
||||||
|
has-flag "^3.0.0"
|
||||||
|
|
||||||
|
uniq@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
|
||||||
|
integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
|
Loading…
Reference in a new issue