Dustin Wilson
5 years ago
commit
bb2c7e27df
5 changed files with 420 additions and 0 deletions
@ -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 |
@ -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. |
@ -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(); |
|||
}); |
|||
}; |
|||
}); |
@ -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" |
|||
} |
|||
} |
@ -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 new issue