├── LICENSE.md ├── README.md ├── bikeshed.js ├── client.js ├── contrast.js ├── css.js ├── dark.js ├── hello.js ├── hex.js ├── hexToRgb.js ├── index.js ├── level.js ├── package.json ├── public └── bundle.js ├── rgb.js ├── view.js └── web.config /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # The MIT License (MIT) 3 | Copyright (c) 2016 Brent Jackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Hello 10k 3 | 4 | https://hello10k.jxnblk.com 5 | 6 | An under 10kB adaptation of [Hello Color](http://jxnblk.com/hello-color) 7 | for [10k Apart](https://a-k-apart.com). 8 | 9 | ## Installing 10 | 11 | This application runs on a Node server. 12 | 13 | ```sh 14 | npm install 15 | ``` 16 | 17 | Build client side bundle: 18 | 19 | ```sh 20 | npm run postinstall 21 | ``` 22 | 23 | Start the node server: 24 | 25 | ```sh 26 | npm start 27 | ``` 28 | 29 | Start the development server: 30 | 31 | ```sh 32 | npm run dev 33 | ``` 34 | 35 | ## About 36 | 37 | This site generates random color pairs that pass a minimum of 4:1 contrast ratio to meet the WCAG's level AA conformance for large text. 38 | Click or refresh the page to generate a new pair. 39 | Using URL parameters, you can bookmark or share any pair of colors from this site. 40 | To see a history of the color pairs from a session, open the developer console in your browser. 41 | 42 | Read more about color contrast recommendations here: 43 | 44 | - [Web Content Accessibility Guidelines](https://www.w3.org/TR/WCAG20/#visual-audio-contrast) 45 | - [Understanding Contrast](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) 46 | 47 | [MIT License](LICENSE.md) 48 | 49 | -------------------------------------------------------------------------------- /bikeshed.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = () => [0, 0, 0] 3 | .map(n => Math.floor(Math.random() * 255)) 4 | 5 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | 2 | const hello = require('./hello') 3 | 4 | const bikeshed = () => [0, 0, 0] 5 | .map(n => Math.floor(Math.random() * 255)) 6 | 7 | const srgb = (n) => n / 255 8 | const lum = ([R, G, B]) => { 9 | const v = (n) => srgb(n) <= .03928 ? srgb(n) / 12.92 : Math.pow((srgb(n) + .055) / 1.055, 2.4) 10 | const r = v(R) 11 | const g = v(G) 12 | const b = v(G) 13 | return 0.2126 * r + 0.7152 * g + 0.0722 * b 14 | } 15 | 16 | const contrast = (a, b) => { 17 | const [l2, l1] = [lum(a), lum(b)].sort() 18 | return (l1 + .05) / (l2 + .05) 19 | } 20 | 21 | const level = n => { 22 | if (n > 7) { 23 | return 'AAA' 24 | } else if (n > 4.5) { 25 | return 'AA' 26 | } else if (n > 3) { 27 | return 'AA Large' 28 | } else { 29 | return 'Fail' 30 | } 31 | } 32 | const rgb = ([r, g, b]) => `rgb(${r}, ${g}, ${b})` 33 | const hex = (rgb) => '#' + rgb.map(v => ('0' + v.toString(16)).slice(-2)).join('') 34 | 35 | const sx = el => s => { 36 | Object.keys(s).forEach(k => { 37 | el.style[k] = s[k] 38 | }) 39 | } 40 | 41 | const log = ({ base, color }) => { 42 | console.log( 43 | '%c%s%c%s', 44 | `padding:4px;color:${rgb(color)};background-color:${rgb(base)}`, 45 | ' Aa ', 46 | 'color:black', 47 | ` ${hex(base)} : ${hex(color)}` 48 | ) 49 | } 50 | 51 | const render = () => { 52 | const base = bikeshed() 53 | const color = hello(base) 54 | const c = Math.round(contrast(base, color) * 100) / 100 55 | 56 | ratio.textContent = `${c} contrast` 57 | score.textContent = level(c) 58 | colorInput.value = hex(color) 59 | baseInput.value = hex(base) 60 | 61 | sx(body)({ 62 | color: rgb(color), 63 | backgroundColor: rgb(base) 64 | }) 65 | sx(titleA)({ 66 | color: rgb(base), 67 | backgroundColor: rgb(color) 68 | }) 69 | 70 | history.pushState({}, null, `?c=${hex(base).replace(/^#/, '')}`) 71 | log({ color, base }) 72 | } 73 | 74 | const stopProp = e => { 75 | e.stopPropagation() 76 | } 77 | body.addEventListener('click', render) 78 | colorInput.addEventListener('click', stopProp) 79 | baseInput.addEventListener('click', stopProp) 80 | 81 | -------------------------------------------------------------------------------- /contrast.js: -------------------------------------------------------------------------------- 1 | 2 | const srgb = (n) => n / 255 3 | const lum = ([R, G, B]) => { 4 | const v = (n) => srgb(n) <= .03928 ? srgb(n) / 12.92 : Math.pow((srgb(n) + .055) / 1.055, 2.4) 5 | const r = v(R) 6 | const g = v(G) 7 | const b = v(G) 8 | return 0.2126 * r + 0.7152 * g + 0.0722 * b 9 | } 10 | 11 | module.exports = (a, b) => { 12 | const [l2, l1] = [lum(a), lum(b)].sort() 13 | return (l1 + .05) / (l2 + .05) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /css.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = ` 3 | *{box-sizing:border-box} 4 | body{ 5 | font-size:1.125rem; 6 | } 7 | .loadButton { 8 | margin:0; 9 | margin-left:-99999px; 10 | font-family:inherit; 11 | font-size:inherit; 12 | font-weight: bold; 13 | padding:.5em; 14 | color:inherit; 15 | background-color:transparent; 16 | border-radius:3px; 17 | border: 1px solid; 18 | -webkit-appearance:none; 19 | -moz-appearance:none; 20 | appearance:none; 21 | } 22 | .loadButton:focus { 23 | margin-left: 0; 24 | outline:none; 25 | box-shadow:0 0 0 2px; 26 | } 27 | `.replace(/\n|\s\s+/g, '') 28 | 29 | -------------------------------------------------------------------------------- /dark.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = ([r, g, b]) => { 3 | const yiq = (r * 299 + g * 587 + b * 114) / 1000 4 | return yiq < 128 5 | } 6 | 7 | -------------------------------------------------------------------------------- /hello.js: -------------------------------------------------------------------------------- 1 | 2 | const contrast = require('./contrast') 3 | const negate = (rgb) => rgb.map(n => 255 - n) 4 | 5 | // rgbToHsl 6 | const rgbToHsl = ([r, g, b]) => { 7 | r /= 255 8 | g /= 255 9 | b /= 255 10 | 11 | const max = Math.max(r, g, b) 12 | const min = Math.min(r, g, b) 13 | 14 | const l = (max + min) / 2 15 | 16 | if (max === min) { 17 | return [0, 0, l] 18 | } else { 19 | const d = max - min 20 | const s = l > 0.5 21 | ? d / (2 - max - min) 22 | : d / (max + min) 23 | 24 | let h 25 | switch (max) { 26 | case r: 27 | h = (g - b) / d // + (g < b ? 6 : 0) 28 | break 29 | case g: 30 | h = (b - r) / d + 2 31 | break 32 | case b: 33 | h = (r - g) / d + 4 34 | break 35 | } 36 | 37 | h /= 6 38 | if (h < 0) h += 1 39 | 40 | // h *= 60 41 | // if (h < 0) h += 360 42 | 43 | return [h, s, l] 44 | } 45 | } 46 | 47 | // hslToRgb 48 | const hslToRgb = ([h, s, l]) => { 49 | if (s === 0) { 50 | return [l, l, l] 51 | } 52 | 53 | const hue2rgb = (p, q, t) => { 54 | t = t < 0 ? t + 1 55 | : t > 1 ? t - 1 56 | : t 57 | 58 | if (t < 1 / 6) return p + (q - p) * 6 * t 59 | if (t < 1 / 2) return q 60 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 61 | 62 | return p 63 | } 64 | 65 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s 66 | const p = 2 * l - q 67 | 68 | const r = hue2rgb(p, q, h + 1 / 3) 69 | const g = hue2rgb(p, q, h) 70 | const b = hue2rgb(p, q, h - 1 / 3) 71 | 72 | return [r, g, b].map(v => Math.round(v * 255)) 73 | } 74 | 75 | let iterations = 0 76 | 77 | const lighten = base => min => (color) => { 78 | const [h, s, l] = rgbToHsl(color) 79 | 80 | if (contrast(base, color) >= min) { 81 | return color 82 | } 83 | 84 | if (l >= 1) { 85 | return null 86 | } 87 | iterations++ 88 | const adjusted = hslToRgb([h, s, l + 1/16]) 89 | 90 | return lighten(base)(min)(adjusted) 91 | } 92 | 93 | const darken = base => min => (color) => { 94 | const [h, s, l] = rgbToHsl(color) 95 | 96 | if (contrast(base, color) >= min) { 97 | return color 98 | } 99 | 100 | if (l <= 0) { 101 | return null 102 | } 103 | 104 | const adjusted = hslToRgb([h, s, l - 1/16]) 105 | iterations++ 106 | 107 | return darken(base)(min)(adjusted) 108 | } 109 | 110 | const resolve = min => base => { 111 | const neg = negate(base) 112 | const [bh, bs, bl] = rgbToHsl(base) 113 | const [h, s, l] = rgbToHsl(neg) 114 | 115 | const color = lighten(base)(min)(neg) || darken(base)(min)(neg) || [0, 0, 0] 116 | return color 117 | } 118 | 119 | module.exports = resolve(4) 120 | 121 | -------------------------------------------------------------------------------- /hex.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (rgb) => '#' + rgb.map(v => ('0' + v.toString(16)).slice(-2)).join('') 3 | 4 | -------------------------------------------------------------------------------- /hexToRgb.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = hex => [0, 0, 0].map((v, i) => { 3 | const i2 = i * 2 4 | return parseInt(hex.replace(/^#/, '').slice(i2, i2 + 2), 16) 5 | }) 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs') 3 | const path = require('path') 4 | const http = require('http') 5 | const url = require('url') 6 | const querystring = require('querystring') 7 | const hello = require('./hello') 8 | const bikeshed = require('./bikeshed') 9 | const view = require('./view') 10 | const hexToRgb = require('./hexToRgb') 11 | 12 | const bundle = fs.readFileSync(path.join(__dirname, 'public/bundle.js'), 'utf8') 13 | 14 | const handleRequest = (req, res) => { 15 | if (/bundle.js/.test(req.url)) { 16 | res.end(bundle) 17 | return 18 | } 19 | 20 | if (/favicon/.test(req.url)) { 21 | res.end('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAA6hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wOS0zMFQxMzowOTo5NjwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjUuMTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpDb21wcmVzc2lvbj41PC90aWZmOkNvbXByZXNzaW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4zMjwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOkNvbG9yU3BhY2U+MTwvZXhpZjpDb2xvclNwYWNlPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MzI8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4K4HHqyQAAAEpJREFUWAljYBgFoyEwGgIjPQQYCQXAnbL//wmpwSev0sWI1w4mfJrpITfqgNEQGA2B0RAYDYHREBgNAXrUuKN2jIbAaAgM7hAAALroBCDTujFCAAAAAElFTkSuQmCC') 22 | return 23 | } 24 | 25 | const { query } = url.parse(req.url, true) 26 | 27 | const base = query.c ? hexToRgb(query.c) : bikeshed() 28 | const color = hello(base) 29 | 30 | res.end( 31 | '' 32 | + view({ 33 | color, 34 | base, 35 | bundle 36 | }) 37 | ) 38 | } 39 | 40 | const server = http.createServer(handleRequest) 41 | 42 | 43 | server.listen(3000, () => { 44 | console.log('Listening on 3000') 45 | }) 46 | 47 | 48 | -------------------------------------------------------------------------------- /level.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = contrast => { 3 | if (contrast > 7) { 4 | return 'AAA' 5 | } else if (contrast > 4.5) { 6 | return 'AA' 7 | } else if (contrast > 3) { 8 | return 'AA Large' 9 | } else { 10 | return 'Fail' 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello10k", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "An under-10kB adaptation of Hello Color for 10k Apart", 6 | "scripts": { 7 | "postinstall": "mkdir -p public && browserify client.js -o public/bundle.js", 8 | "start": "node index.js", 9 | "dev": "nodemon index.js" 10 | }, 11 | "author": "Brent Jackson", 12 | "license": "MIT", 13 | "dependencies": { 14 | "h0": "^1.0.0", 15 | "min-document": "^2.18.1", 16 | "serve-static": "^1.11.1" 17 | }, 18 | "devDependencies": { 19 | "browserify": "^13.1.0", 20 | "nodemon": "^1.10.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /public/bundle.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o [0, 0, 0] 6 | .map(n => Math.floor(Math.random() * 255)) 7 | 8 | const srgb = (n) => n / 255 9 | const lum = ([R, G, B]) => { 10 | const v = (n) => srgb(n) <= .03928 ? srgb(n) / 12.92 : Math.pow((srgb(n) + .055) / 1.055, 2.4) 11 | const r = v(R) 12 | const g = v(G) 13 | const b = v(G) 14 | return 0.2126 * r + 0.7152 * g + 0.0722 * b 15 | } 16 | 17 | const contrast = (a, b) => { 18 | const [l2, l1] = [lum(a), lum(b)].sort() 19 | return (l1 + .05) / (l2 + .05) 20 | } 21 | 22 | const level = n => { 23 | if (n > 7) { 24 | return 'AAA' 25 | } else if (n > 4.5) { 26 | return 'AA' 27 | } else if (n > 3) { 28 | return 'AA Large' 29 | } else { 30 | return 'Fail' 31 | } 32 | } 33 | const rgb = ([r, g, b]) => `rgb(${r}, ${g}, ${b})` 34 | const hex = (rgb) => '#' + rgb.map(v => ('0' + v.toString(16)).slice(-2)).join('') 35 | 36 | const sx = el => s => { 37 | Object.keys(s).forEach(k => { 38 | el.style[k] = s[k] 39 | }) 40 | } 41 | 42 | const log = ({ base, color }) => { 43 | console.log( 44 | '%c%s%c%s', 45 | `padding:4px;color:${rgb(color)};background-color:${rgb(base)}`, 46 | ' Aa ', 47 | 'color:black', 48 | ` ${hex(base)} : ${hex(color)}` 49 | ) 50 | } 51 | 52 | const render = () => { 53 | const base = bikeshed() 54 | const color = hello(base) 55 | const c = Math.round(contrast(base, color) * 100) / 100 56 | 57 | ratio.textContent = `${c} contrast` 58 | score.textContent = level(c) 59 | colorInput.value = hex(color) 60 | baseInput.value = hex(base) 61 | 62 | sx(body)({ 63 | color: rgb(color), 64 | backgroundColor: rgb(base) 65 | }) 66 | sx(titleA)({ 67 | color: rgb(base), 68 | backgroundColor: rgb(color) 69 | }) 70 | 71 | history.pushState({}, null, `?c=${hex(base).replace(/^#/, '')}`) 72 | log({ color, base }) 73 | } 74 | 75 | const stopProp = e => { 76 | e.stopPropagation() 77 | } 78 | body.addEventListener('click', render) 79 | colorInput.addEventListener('click', stopProp) 80 | baseInput.addEventListener('click', stopProp) 81 | 82 | 83 | },{"./hello":3}],2:[function(require,module,exports){ 84 | 85 | const srgb = (n) => n / 255 86 | const lum = ([R, G, B]) => { 87 | const v = (n) => srgb(n) <= .03928 ? srgb(n) / 12.92 : Math.pow((srgb(n) + .055) / 1.055, 2.4) 88 | const r = v(R) 89 | const g = v(G) 90 | const b = v(G) 91 | return 0.2126 * r + 0.7152 * g + 0.0722 * b 92 | } 93 | 94 | module.exports = (a, b) => { 95 | const [l2, l1] = [lum(a), lum(b)].sort() 96 | return (l1 + .05) / (l2 + .05) 97 | } 98 | 99 | 100 | },{}],3:[function(require,module,exports){ 101 | 102 | const contrast = require('./contrast') 103 | const negate = (rgb) => rgb.map(n => 255 - n) 104 | 105 | // rgbToHsl 106 | const rgbToHsl = ([r, g, b]) => { 107 | r /= 255 108 | g /= 255 109 | b /= 255 110 | 111 | const max = Math.max(r, g, b) 112 | const min = Math.min(r, g, b) 113 | 114 | const l = (max + min) / 2 115 | 116 | if (max === min) { 117 | return [0, 0, l] 118 | } else { 119 | const d = max - min 120 | const s = l > 0.5 121 | ? d / (2 - max - min) 122 | : d / (max + min) 123 | 124 | let h 125 | switch (max) { 126 | case r: 127 | h = (g - b) / d // + (g < b ? 6 : 0) 128 | break 129 | case g: 130 | h = (b - r) / d + 2 131 | break 132 | case b: 133 | h = (r - g) / d + 4 134 | break 135 | } 136 | 137 | h /= 6 138 | if (h < 0) h += 1 139 | 140 | // h *= 60 141 | // if (h < 0) h += 360 142 | 143 | return [h, s, l] 144 | } 145 | } 146 | 147 | // hslToRgb 148 | const hslToRgb = ([h, s, l]) => { 149 | if (s === 0) { 150 | return [l, l, l] 151 | } 152 | 153 | const hue2rgb = (p, q, t) => { 154 | t = t < 0 ? t + 1 155 | : t > 1 ? t - 1 156 | : t 157 | 158 | if (t < 1 / 6) return p + (q - p) * 6 * t 159 | if (t < 1 / 2) return q 160 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 161 | 162 | return p 163 | } 164 | 165 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s 166 | const p = 2 * l - q 167 | 168 | const r = hue2rgb(p, q, h + 1 / 3) 169 | const g = hue2rgb(p, q, h) 170 | const b = hue2rgb(p, q, h - 1 / 3) 171 | 172 | return [r, g, b].map(v => Math.round(v * 255)) 173 | } 174 | 175 | let iterations = 0 176 | 177 | const lighten = base => min => (color) => { 178 | const [h, s, l] = rgbToHsl(color) 179 | 180 | if (contrast(base, color) >= min) { 181 | return color 182 | } 183 | 184 | if (l >= 1) { 185 | return null 186 | } 187 | iterations++ 188 | const adjusted = hslToRgb([h, s, l + 1/16]) 189 | 190 | return lighten(base)(min)(adjusted) 191 | } 192 | 193 | const darken = base => min => (color) => { 194 | const [h, s, l] = rgbToHsl(color) 195 | 196 | if (contrast(base, color) >= min) { 197 | return color 198 | } 199 | 200 | if (l <= 0) { 201 | return null 202 | } 203 | 204 | const adjusted = hslToRgb([h, s, l - 1/16]) 205 | iterations++ 206 | 207 | return darken(base)(min)(adjusted) 208 | } 209 | 210 | const resolve = min => base => { 211 | const neg = negate(base) 212 | const [bh, bs, bl] = rgbToHsl(base) 213 | const [h, s, l] = rgbToHsl(neg) 214 | 215 | const color = lighten(base)(min)(neg) || darken(base)(min)(neg) || [0, 0, 0] 216 | return color 217 | } 218 | 219 | module.exports = resolve(4) 220 | 221 | 222 | },{"./contrast":2}]},{},[1]); 223 | -------------------------------------------------------------------------------- /rgb.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = ([r, g, b]) => `rgb(${r}, ${g}, ${b})` 3 | 4 | -------------------------------------------------------------------------------- /view.js: -------------------------------------------------------------------------------- 1 | 2 | global.document = require('min-document') 3 | global.Element = require('min-document/dom-element') 4 | 5 | const h = require('h0').default 6 | const { 7 | div, 8 | h1, 9 | ul, 10 | pre, 11 | button 12 | } = require('h0/dist/elements') 13 | const getContrast = require('./contrast') 14 | const getLevel = require('./level') 15 | const rgb = require('./rgb') 16 | const hex = require('./hex') 17 | const dark = require('./dark') 18 | const css = require('./css') 19 | 20 | const li = h('li') 21 | 22 | const a = h('a')({ 23 | style: { 24 | fontWeight: 'bold', 25 | color: 'inherit' 26 | } 27 | }) 28 | 29 | const label = h('label')({ 30 | style: { 31 | position: 'absolute', 32 | height: 1, 33 | width: 1, 34 | overflow: 'hidden', 35 | clip: 'rect(1px, 1px, 1px, 1px)' 36 | } 37 | }) 38 | 39 | const input = h('input')({ 40 | style: { 41 | fontFamily: 'Menlo, monospace', 42 | fontSize: 'inherit', 43 | boxSizing: 'border-box', 44 | display: 'block', 45 | padding: '.5em 0', 46 | border: 0, 47 | color: 'inherit', 48 | backgroundColor: 'transparent', 49 | appearance: 'none', 50 | } 51 | }) 52 | 53 | const p = h('p')({ 54 | style: { 55 | maxWidth: '40em' 56 | } 57 | }) 58 | 59 | const grid = (props = {}) => div(Object.assign(props, { 60 | style: Object.assign({ 61 | boxSizing: 'border-box', 62 | display: 'inline-block', 63 | verticalAlign: 'top', 64 | width: 384, 65 | maxWidth: '100%', 66 | padding: 24, 67 | }, props.style) 68 | })) 69 | 70 | const loadButton = grid()( 71 | h('button')({ 72 | class: 'loadButton', 73 | })('Change Colors') 74 | ) 75 | 76 | const head = h('head')( 77 | h('meta')({ 78 | charset: 'utf-8' 79 | })(), 80 | h('meta')({ 81 | name: 'viewport', 82 | content: 'width=device-width,initial-scale=1' 83 | })(), 84 | h('title')('Hello 10k'), 85 | h('style')(css) 86 | ) 87 | 88 | const body = ({ color, base }) => h('body')({ 89 | id: 'body', 90 | style: { 91 | fontFamily: '-apple-system,BlinkMacSystemFont,sans-serif', 92 | lineHeight: 1.5, 93 | margin: 0, 94 | color: rgb(color), 95 | backgroundColor: rgb(base), 96 | cursor: 'pointer', 97 | transitionProperty: 'color, background-color', 98 | transitionDuration: '.2s, .8s', 99 | transitionTimingFunction: 'ease-out' 100 | } 101 | }) 102 | 103 | const main = (props) => h('main')({ 104 | style: { 105 | boxSizing: 'border-box', 106 | minHeight: '100vh', 107 | padding: 16, 108 | WebkitUserSelect: 'none', 109 | MozUserSelect: 'none', 110 | userSelect: 'none' 111 | } 112 | })( 113 | loadButton, 114 | title(props), 115 | grid({ 116 | id: 'ratio', 117 | style: { 118 | fontSize: '2em', 119 | fontWeight: 'bold' 120 | } 121 | })(`${props.contrast}:1 contrast`), 122 | grid({ 123 | id: 'score', 124 | style: { 125 | fontSize: '2em', 126 | fontWeight: 'bold' 127 | } 128 | })(getLevel(props.contrast)), 129 | grid()( 130 | label('Color'), 131 | input({ 132 | id: 'colorInput', 133 | readonly: true, 134 | name: 'color', 135 | value: hex(props.color) 136 | })(), 137 | label('Background Color'), 138 | input({ 139 | id: 'baseInput', 140 | readonly: true, 141 | name: 'base', 142 | value: hex(props.base) 143 | })() 144 | ), 145 | footer(props) 146 | ) 147 | 148 | const title = ({ color, base }) => h1({ 149 | id: 'title', 150 | style: { 151 | fontSize: '3em', 152 | textTransform: 'uppercase', 153 | letterSpacing: '.2em', 154 | display: 'flex', 155 | flexWrap: 'wrap' 156 | } 157 | })( 158 | grid({ 159 | id: 'titleA', 160 | style: { 161 | color: rgb(base), 162 | backgroundColor: rgb(color), 163 | transitionProperty: 'color, background-color', 164 | transitionDuration: '.8s, .2s', 165 | transitionTimingFunction: 'ease-out' 166 | } 167 | })('Hello'), 168 | grid({ 169 | id: 'titleB' 170 | })('10k') 171 | ) 172 | 173 | const footer = ({ color, base }) => { 174 | return h('footer')({ id: 'footer' })( 175 | grid()( 176 | p('This site generates random color pairs that pass a minimum of 4:1 contrast ratio to meet the WCAG’s level AA conformance for large text. Click or refresh the page to generate a new pair. Using URL parameters, you can bookmark or share any pair of colors from this site. To see a history of the color pairs from a session, open the developer console in your browser.') 177 | ), 178 | grid()( 179 | p('Whether you’re getting older, have a cognitive disability, are sitting next to a window, or are using your phone outside in daylight, color contrast is an essential part of universal Web accessibility. While visual design trends come and go, sufficiently-contrasted and readable text will always be an indication of a thoughtful, well-designed website.') 180 | ), 181 | grid()( 182 | p('Read more about the color contrast minimum here: '), 183 | ul( 184 | li( 185 | a({ href: 'https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html' })('Understanding Contrast') 186 | ), 187 | li( 188 | a({ href: 'https://www.w3.org/TR/WCAG20/#visual-audio-contrast' })('Web Content Accessibility Guidelines') 189 | ) 190 | ) 191 | ), 192 | grid()( 193 | ul( 194 | li( 195 | a({ href: 'https://github.com/jxnblk/hello10k' })('GitHub') 196 | ), 197 | li( 198 | a({ href: 'http://jxnblk.com' })('Made by Jxnblk') 199 | ) 200 | ) 201 | ) 202 | ) 203 | } 204 | 205 | 206 | const view = (props) => { 207 | const { color, base, bundle } = props 208 | const contrast = Math.floor(getContrast(color, base) * 100) / 100 209 | 210 | return String(h('html')( 211 | head, 212 | body(props)( 213 | main(Object.assign(props, { 214 | contrast 215 | })), 216 | h('script')({ 217 | __html: bundle 218 | })() 219 | )) 220 | ) 221 | } 222 | 223 | module.exports = view 224 | 225 | -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 60 | 61 | 62 | 68 | 69 | 70 | 71 | --------------------------------------------------------------------------------