From 89c3ddb84b5c4c9bf310c199c1f6c200e23147d5 Mon Sep 17 00:00:00 2001 From: Christian Lawson-Perfect Date: Sun, 9 Feb 2025 20:19:13 +0000 Subject: [PATCH] first commit --- .gitignore | 1 + LICENSE | 54 ++++ README.md | 0 apca-w3.js | 763 +++++++++++++++++++++++++++++++++++++++++++++++ auto_contrast.js | 34 +++ color-picker.js | 158 ++++++++++ colour_names.js | 30 ++ convert.js | 219 ++++++++++++++ culori.mjs | 4 + index.html | 136 +++++++++ scheme.css | 229 ++++++++++++++ scheme.js | 82 +++++ script.js | 134 +++++++++ style.css | 42 +++ 14 files changed, 1886 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 apca-w3.js create mode 100644 auto_contrast.js create mode 100644 color-picker.js create mode 100644 colour_names.js create mode 100644 convert.js create mode 100644 culori.mjs create mode 100644 index.html create mode 100644 scheme.css create mode 100644 scheme.js create mode 100644 script.js create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bc0f76 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.make.* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b2b4c42 --- /dev/null +++ b/LICENSE @@ -0,0 +1,54 @@ +The LICENSE file for any project gives credit to the creator/author of the +project, copyright information for the project, and the legal terms under +which it's being shared. In other words, this is us using an MIT license to +say "we wrote this and you can do whatever you want with it." + +****************************************************************************** +~glitch-hello-website +****************************************************************************** +MIT License + +Copyright (c) 2021, Glitch, Inc. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + + + + +****************************************************************************** + +THIRD-PARTY SOFTWARE +This is all the software we used to build this starter project. All of these +licenses are compatible with the license above. We've included links so you +can learn more if you want. + +1. HK Grotesk: The font we're using. + + +****************************************************************************** +1. HK Grotesk +URL: https://hanken.co/products/hk-grotesk +****************************************************************************** +HK Grotesk was designed by Hanken Design Co. It is shared using a SIL OFL +license. Full license text can be found at: + +https://hanken.co/pages/web-fonts-eula +****************************************************************************** +END, HK Grotesk +****************************************************************************** \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/apca-w3.js b/apca-w3.js new file mode 100644 index 0000000..4bace53 --- /dev/null +++ b/apca-w3.js @@ -0,0 +1,763 @@ +/////////////////////////////////////////////////////////////////////////////// +/** @preserve +///// SAPC APCA - Advanced Perceptual Contrast Algorithm +///// Beta 0.1.9 W3 • contrast function only +///// DIST: W3 • Revision date: July 3, 2022 +///// Function to parse color values and determine Lc contrast +///// Copyright © 2019-2022 by Andrew Somers. All Rights Reserved. +///// LICENSE: W3 LICENSE +///// CONTACT: Please use the ISSUES or DISCUSSIONS tab at: +///// https://github.com/Myndex/SAPC-APCA/ +///// +///// ------------------------------------------------------ +///// Modified 2024-08-30 by Christian Lawson-Perfect +///// christian@lawson-perfect.uk +///// to use culori.js instead of colorparsley +///// ------------------------------------------------------ +///// +/////////////////////////////////////////////////////////////////////////////// +///// +///// MINIMAL IMPORTS: +///// import { APCAcontrast, sRGBtoY, displayP3toY, +///// calcAPCA, fontLookupAPCA } from 'apca-w3'; +///// import { colorParsley } from 'colorparsley'; +///// +///// FORWARD CONTRAST USAGE: +///// Lc = APCAcontrast( sRGBtoY( TEXTcolor ) , sRGBtoY( BACKGNDcolor ) ); +///// Where the colors are sent as an rgba array [255,255,255,1] +///// +///// Retrieving an array of font sizes for the contrast: +///// fontArray = fontLookupAPCA(Lc); +///// +///// Live Demonstrator at https://www.myndex.com/APCA/ +// */ +/////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +///// +///// SAPC Method and APCA Algorithm +///// W3 Licensed Version: https://github.com/Myndex/apca-w3 +///// GITHUB MAIN REPO: https://github.com/Myndex/SAPC-APCA +///// DEVELOPER SITE: https://git.myndex.com/ +///// +///// Acknowledgments and Thanks To: +///// • This project references the research & work of M.Fairchild, R.W.Hunt, +///// Drs. Bailey/Lovie-Kitchin, G.Legge, A.Arditi, M.Stone, C.Poynton, +///// L.Arend, M.Luo, E.Burns, R.Blackwell, P.Barton, M.Brettel, and many +///// others — see refs at https://www.myndex.com/WEB/WCAG_CE17polarity +///// • Bruce Bailey of USAccessBoard for his encouragement, ideas, & feedback +///// • Chris Lilly of W3C for continued review, examination, & oversight +///// • Chris Loiselle of Oracle for getting us back on track in a pandemic +///// • The many volunteer test subjects for participating in the studies. +///// • The many early adopters, beta testers, and code/issue contributors +///// • Principal research conducted at Myndex by A.Somers. +///// +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +///// +///// ***** SAPC BLOCK ***** +///// +///// For Evaluations, refer to this as: SAPC-8, 0.0.98G-series constant 4g +///// SAPC • S-LUV Advanced Predictive Color +///// +///// SIMPLE VERSION — Only the basic APCA contrast predictor. +///// +///// Included Extensions & Model Features in this file: +///// • SAPC-8 Core Contrast (Base APCA, non-clinical use only) +///// • G series constants, group "G-4g" using a 2.4 monitor exponent +///// • sRGB to Y, parses numeric sRGB color to luminance +///// • SoftToe black level soft clamp and flare compensation. +///// +///// +//////////////////////////////////////////////////////////////////////////////// +///// +///// DISCLAIMER AND LIMITATIONS OF USE +///// APCA is an embodiment of certain suprathreshold contrast +///// prediction technologies and it is licensed to the W3 on a +///// limited basis for use in certain specific accessibility +///// guidelines for web content only. APCA may be used for +///// predicting colors for web content use without royalty. +///// +///// However, Any such license excludes other use cases +///// not related to web content. Prohibited uses include +///// medical, clinical evaluation, human safety related, +///// aerospace, transportation, military applications, +///// and uses which are not specific to web based content +///// presented on self-illuminated displays or devices. +///// +//////////////////////////////////////////////////////////////////////////////// + +////////// APCA 0.1.9 G 4g USAGE /////////////////////////////////////////// +/// +/// The API for "APCA 0.1.9" is trivially simple. +/// Send text and background sRGB numeric values to the sRGBtoY() function, +/// and send the resulting text-Y and background-Y to the APCAcontrast function +/// it returns a signed float with the numeric Lc contrast result. +/// +/// The two inputs are TEXT color and BACKGROUND color in that order. +/// Each must be a numeric NOT a string, as this simple version has +/// no string parsing utilities. EXAMPLE: +/// ________________________________________________________________________ +/// +/// txtColor = colorParsley(0x123456); // color of the text +/// bgColor = colorParsley(0xabcdef); // color for the background +/// +/// contrastLc = APCAcontrast( sRGBtoY(txtColor) , sRGBtoY(bgColor) ); +/// ________________________________________________________________________ +/// +/// ********** QUICK START ********** +/// +/// Each color must be a 24bit color (8 bit per channel) as a single integer +/// (or 0x) sRGB encoded color, i.e. White is either the integer 16777216 or +/// the hex 0xffffff. A float is returned with a positive or negative value. +/// Negative values mean light text and a dark background, positive values +/// mean dark text and a light background. 60.0, or -60.0 is a contrast +/// "sort of like" the old WCAG 2's 4.5:1. NOTE: the total range is now less +/// than ± 110, so output can be rounded to a signed INT but DO NOT output +/// an absolute value - light text on dark BG should return a negative number. +/// +/// ***** IMPORTANT: Do Not Mix Up Text and Background inputs. ***** +/// **************** APCA is polarity sensitive! ***************** +/// +//////////////////////////////////////////////////////////////////////////////// + + +//////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +///// BEGIN APCA 0.1.9 BLOCK \///////////////////////////////////// +//// \/////////////////////////////////// +/// \///////////////////////////////// +// \/////////////////////////////// + + +///// DEPENDENCIES ///// + +// The following imports are not needed for the main APCA function, +// but are needed for the shortcut/alias calcAPCA(), and for the +// future invertAPCA function, which examines hue. + + //// (add slash to line start for local test mode, remove before push) +/* //// LOCAL TESTING SWITCH for using test.html + import{colorParsley}from'../node_modules/colorparsley/src/colorparsley.js'; +/*/ //// TOGGLE + import * as culori from './culori.mjs'; + + const convertToRGB = culori.converter('rgb'); + function parse_rgb(def) { + const col = culori.parse(def); + const {r, g, b, alpha} = convertToRGB(col); + return [255*r, 255*g, 255*b, alpha == undefined ? 1 : alpha]; + } +// */ //// END LOCAL TESTING SWITCH + + +///// Module Scope Object Containing Constants ///// +///// APCA 0.0.98G - 4g - W3 Compatible Constants + +///// 𝒦 SA98G /////////////////////////////////// + const SA98G = { + + mainTRC: 2.4, // 2.4 exponent for emulating actual monitor perception + + // For reverseAPCA + get mainTRCencode() { return 1 / this.mainTRC }, + + // sRGB coefficients + sRco: 0.2126729, + sGco: 0.7151522, + sBco: 0.0721750, + + // G-4g constants for use with 2.4 exponent + normBG: 0.56, + normTXT: 0.57, + revTXT: 0.62, + revBG: 0.65, + + // G-4g Clamps and Scalers + blkThrs: 0.022, + blkClmp: 1.414, + scaleBoW: 1.14, + scaleWoB: 1.14, + loBoWoffset: 0.027, + loWoBoffset: 0.027, + deltaYmin: 0.0005, + loClip: 0.1, + + ///// MAGIC NUMBERS for UNCLAMP, for use with 0.022 & 1.414 ///// + // Magic Numbers for reverseAPCA + mFactor: 1.94685544331710, + get mFactInv() { return 1 / this.mFactor}, + mOffsetIn: 0.03873938165714010, + mExpAdj: 0.2833433964208690, + get mExp() { return this.mExpAdj / this.blkClmp}, + mOffsetOut: 0.3128657958707580, + } + + + + +////////////////////////////////////////////////////////////////////////////// +////////// APCA CALCULATION FUNCTIONS \///////////////////////////////////// + +////////// ƒ APCAcontrast() //////////////////////////////////////////// +export function APCAcontrast (txtY,bgY,places = -1) { + // send linear Y (luminance) for text and background. + // txtY and bgY must be between 0.0-1.0 + // IMPORTANT: Do not swap, polarity is important. + + const icp = [0.0,1.1]; // input range clamp / input error check + + if(isNaN(txtY)||isNaN(bgY)||Math.min(txtY,bgY)icp[1]){ + return 0.0; // return zero on error + // return 'error'; // optional string return for error + }; + +////////// SAPC LOCAL VARS ///////////////////////////////////////// + + let SAPC = 0.0; // For raw SAPC values + let outputContrast = 0.0; // For weighted final values + let polCat = 'BoW'; // Alternate Polarity Indicator. N normal R reverse + + // TUTORIAL + + // Use Y for text and BG, and soft clamp black, + // return 0 for very close luminances, determine + // polarity, and calculate SAPC raw contrast + // Then scale for easy to remember levels. + + // Note that reverse contrast (white text on black) + // intentionally returns a negative number + // Proper polarity is important! + +////////// BLACK SOFT CLAMP //////////////////////////////////////// + + // Soft clamps Y for either color if it is near black. + txtY = (txtY > SA98G.blkThrs) ? txtY : + txtY + Math.pow(SA98G.blkThrs - txtY, SA98G.blkClmp); + bgY = (bgY > SA98G.blkThrs) ? bgY : + bgY + Math.pow(SA98G.blkThrs - bgY, SA98G.blkClmp); + + ///// Return 0 Early for extremely low ∆Y + if ( Math.abs(bgY - txtY) < SA98G.deltaYmin ) { return 0.0; } + + +////////// APCA/SAPC CONTRAST - LOW CLIP (W3 LICENSE) /////////////// + + if ( bgY > txtY ) { // For normal polarity, black text on white (BoW) + + // Calculate the SAPC contrast value and scale + SAPC = ( Math.pow(bgY, SA98G.normBG) - + Math.pow(txtY, SA98G.normTXT) ) * SA98G.scaleBoW; + + // Low Contrast smooth rollout to prevent polarity reversal + // and also a low-clip for very low contrasts + outputContrast = (SAPC < SA98G.loClip) ? 0.0 : SAPC - SA98G.loBoWoffset; + + } else { // For reverse polarity, light text on dark (WoB) + // WoB should always return negative value. + polCat = 'WoB'; + + SAPC = ( Math.pow(bgY, SA98G.revBG) - + Math.pow(txtY, SA98G.revTXT) ) * SA98G.scaleWoB; + + outputContrast = (SAPC > -SA98G.loClip) ? 0.0 : SAPC + SA98G.loWoBoffset; + } + + // return Lc (lightness contrast) as a signed numeric value + // Round to the nearest whole number as string is optional. + // Rounded can be a signed INT as output will be within ± 127 + // places = -1 returns signed float, 1 or more set that many places + // 0 returns rounded string, uses BoW or WoB instead of minus sign + + if(places < 0 ){ // Default (-1) number out, all others are strings + return outputContrast * 100.0; + } else if(places == 0 ){ + return Math.round(Math.abs(outputContrast)*100.0)+''+polCat+''; + } else if(Number.isInteger(places)){ + return (outputContrast * 100.0).toFixed(places); + } else { return 0.0 } + +} // End APCAcontrast() + + + + +/* SWITCH -- WORK IN PROGRESS DO NOT USE +////////// ƒ invertAPCA() ////////////////////////////////////////////////// +export function invertAPCA ( + {knownColor: [128,128,128], knownType: 'bg', targetCnst: 75, + returnAs: 'object', unknownType: 'txt', hueInvert: false, + hueRange: 5, preserveSat: false }) { + + //if (Math.abs(targetCnst) < 15) { return false }; // abs contrast must be > 15 + + let knownY = sRGBtoY (knownColor); + let unknownY = knownY, knownExp, unknownExp; + let min,max,knownLs,isBG = true; + + + if (knownType == 'bg' || knownType == 'background') { + knownLs = Math.pow(knownY, ); + black = APCAcontrast(0,knownY); + white = APCAcontrast(1,knownY); + } else if (knownType == 'txt' || knownType == 'text') { + isBG = false; + black = APCAcontrast(knownY,0); + white = APCAcontrast(knownY,1); + } else { return false } // return false on error + + +///// APCA 0.0.98G - 4g - W3 Compatible Constants //////////////////// + + const scale = contrast > 0 ? SA98G.scaleBoW : SA98G.scaleWoB; + const offset = contrast > 0 ? SA98G.loBoWoffset : -SA98G.loWoBoffset; + + + targetCnst = ( parseFloat(targetCnst) * 0.01 + offset ) / scale; + + // Soft clamps Y if it is near black. + knownY = (knownY > SA98G.blkThrs) ? knownY : + knownY + Math.pow(SA98G.blkThrs - knownY, SA98G.blkClmp); + + // set the known and unknown exponents + if (isBG) { + knownExp = targetCnst > 0 ? SA98G.normBG : SA98G.revBG; + unknownExp = targetCnst > 0 ? SA98G.normTXT : SA98G.revTXT; + unknownY = Math.pow( Math.pow(knownY,knownExp) - targetCnst, 1/unknownExp ); + if (isNaN(unknownY)) return false; + } else if (!isBG) { + knownExp = targetCnst > 0 ? SA98G.normTXT : SA98G.revTXT; + unknownExp = targetCnst > 0 ? SA98G.normBG : SA98G.revBG; + unknownY = Math.pow(targetCnst + Math.pow(knownY,knownExp), 1/unknownExp ); + if (isNaN(unknownY)) return false; + } + + //return contrast +'----'+unknownY; + + if (unknownY > 1.06 || unknownY < 0) { return false } // return false on overflow + // if (unknownY < 0) { return false } // return false on underflow + //unknownY = Math.max(unknownY,0.0); + + // unclamp + unknownY = (unknownY > SA98G.blkThrs) ? unknownY : + (Math.pow(((unknownY + SA98G.mOffsetIn)m* SA98G.mFactor), + SA98G.mExp) * SA98G.mFactInv) - SA98G.mOffsetOut; + +// unknownY - 0.22 * Math.pow(unknownY*0.5, 1/blkClmp); + + unknownY = Math.max(Math.min(unknownY,1.0),0.0); + + let testedCnst = (isBG) ? APCAcontrast(unknownY,knownY) : + APCAcontrast(knownY,unknownY); + + if (returnAs === 'object') { + let hexB = ( Math.round(Math.pow(unknownY,SA98G.mainTRCencode) * 255) + ).toString(16).padStart(2,'0'); + hexB = '#' + hexB + hexB + hexB; + return {color: hexB, Lc: testedCnst, whiteLc: white, blackLc: black}; + } else if (returnAs === 'hex') { + let hexB = ( Math.round(Math.pow(unknownY,SA98G.mainTRCencode) * 255) + ).toString(16).padStart(2,'0'); + return '#' + hexB + hexB + hexB; + } else if (returnAs === 'array') { + let colorB = Math.round(Math.pow(unknownY,SA98G.mainTRCencode) * 255); + let retUse = (knownType == 'bg') ? 'txtColor' : 'bgColor' + return [colorB,colorB,colorB,1,retUse]; + } else if (returnAs === 'Y' || returnAs === 'y') { + return Math.max(0.0,unknownY); + } else { return false } // return knownY on error +} +// */ // END SWITCH + + + + +////////// ƒ reverseAPCA() DEPRECATED SOON /////////////////////////////// +export function reverseAPCA (contrast = 0,knownY = 1.0, + knownType = 'bg',returnAs = 'hex') { + + if (Math.abs(contrast) < 9) { return false }; // abs contrast must be > 9 + + let unknownY = knownY, knownExp, unknownExp; + +///// APCA 0.0.98G - 4g - W3 Compatible Constants //////////////////// + + const scale = contrast > 0 ? SA98G.scaleBoW : SA98G.scaleWoB; + const offset = contrast > 0 ? SA98G.loBoWoffset : -SA98G.loWoBoffset; + + + contrast = ( parseFloat(contrast) * 0.01 + offset ) / scale; + + // Soft clamps Y if it is near black. + knownY = (knownY > SA98G.blkThrs) ? knownY : + knownY + Math.pow(SA98G.blkThrs - knownY, SA98G.blkClmp); + + // set the known and unknown exponents + if (knownType == 'bg' || knownType == 'background') { + knownExp = contrast > 0 ? SA98G.normBG : SA98G.revBG; + unknownExp = contrast > 0 ? SA98G.normTXT : SA98G.revTXT; + unknownY = Math.pow( Math.pow(knownY,knownExp) - contrast, 1/unknownExp ); + if (isNaN(unknownY)) return false; + } else if (knownType == 'txt' || knownType == 'text') { + knownExp = contrast > 0 ? SA98G.normTXT : SA98G.revTXT; + unknownExp = contrast > 0 ? SA98G.normBG : SA98G.revBG; + unknownY = Math.pow(contrast + Math.pow(knownY,knownExp), 1/unknownExp ); + if (isNaN(unknownY)) return false; + } else { return false } // return false on error + + //return contrast +'----'+unknownY; + + if (unknownY > 1.06 || unknownY < 0) { return false } // return false on overflow + // if (unknownY < 0) { return false } // return false on underflow + //unknownY = Math.max(unknownY,0.0); + + // unclamp + unknownY = (unknownY > SA98G.blkThrs) ? unknownY : + (Math.pow(((unknownY + SA98G.mOffsetIn) * SA98G.mFactor), + SA98G.mExp) * SA98G.mFactInv) - SA98G.mOffsetOut; + +// unknownY - 0.22 * Math.pow(unknownY*0.5, 1/blkClmp); + + unknownY = Math.max(Math.min(unknownY,1.0),0.0); + + if (returnAs === 'hex') { + let hexB = ( Math.round(Math.pow(unknownY,SA98G.mainTRCencode) * 255) + ).toString(16).padStart(2,'0'); + + return '#' + hexB + hexB + hexB; + } else if (returnAs === 'color') { + let colorB = Math.round(Math.pow(unknownY,SA98G.mainTRCencode) * 255); + let retUse = (knownType == 'bg') ? 'txtColor' : 'bgColor' + return [colorB,colorB,colorB,1,retUse]; + } else if (returnAs === 'Y' || returnAs === 'y') { + return Math.max(0.0,unknownY); + } else { return false } // return knownY on error +} + + + + +////////// ƒ calcAPCA() ///////////////////////////////////////////// +export function calcAPCA (textColor, bgColor, places = -1, round = true) { + + // Note that this function requires culori !! + let bgClr = parse_rgb(bgColor); + let txClr = parse_rgb(textColor); + let hasAlpha = (txClr[3] == '' || txClr[3] == 1) ? false : true ; + + if (hasAlpha) { txClr = alphaBlend( txClr, bgClr, round); }; + + return APCAcontrast( sRGBtoY(txClr), sRGBtoY(bgClr), places) +} // End calcAPCA() + + + + +////////////////////////////////////////////////////////////////////////////// +////////// ƒ fontLookupAPCA() 0.1.7 (G) \//////////////////////////////// +///////// \////////////////////////////// + +export function fontLookupAPCA (contrast,places=2) { + +//////////////////////////////////////////////////////////////////////////// +///// CONTRAST * FONT WEIGHT & SIZE ///////////////////////////////////// + +// Font size interpolations. Here the chart was re-ordered to put +// the main contrast levels each on one line, instead of font size per line. +// First column is LC value, then each following column is font size by weight + +// G G G G G G Public Beta 0.1.7 (G) • MAY 28 2022 + +// Lc values under 70 should have Lc 15 ADDED if used for body text +// All font sizes are in px and reference font is Barlow + +// 999: prohibited - too low contrast +// 777: NON TEXT at this minimum weight stroke +// 666 - this is for spot text, not fluent-Things like copyright or placeholder. +// 5xx - minimum font at this weight for content, 5xx % 500 for font-size +// 4xx - minimum font at this weight for any purpose], 4xx % 400 for font-size + +// MAIN FONT SIZE LOOKUP + +//// ASCENDING SORTED Public Beta 0.1.7 (G) • MAY 28 2022 //// + +//// Lc 45 * 0.2 = 9 which is the index for the row for Lc 45 + +// MAIN FONT LOOKUP May 28 2022 EXPANDED +// Sorted by Lc Value +// First row is standard weights 100-900 +// First column is font size in px +// All other values are the Lc contrast +// 999 = too low. 777 = non-text and spot text only + + +const fontMatrixAscend = [ + ['Lc',100,200,300,400,500,600,700,800,900], + [0,999,999,999,999,999,999,999,999,999], + [10,999,999,999,999,999,999,999,999,999], + [15,777,777,777,777,777,777,777,777,777], + [20,777,777,777,777,777,777,777,777,777], + [25,777,777,777,120,120,108,96,96,96], + [30,777,777,120,108,108,96,72,72,72], + [35,777,120,108,96,72,60,48,48,48], + [40,120,108,96,60,48,42,32,32,32], + [45,108,96,72,42,32,28,24,24,24], + [50,96,72,60,32,28,24,21,21,21], + [55,80,60,48,28,24,21,18,18,18], + [60,72,48,42,24,21,18,16,16,18], + [65,68,46,32,21.75,19,17,15,16,18], + [70,64,44,28,19.5,18,16,14.5,16,18], + [75,60,42,24,18,16,15,14,16,18], + [80,56,38.25,23,17.25,15.81,14.81,14,16,18], + [85,52,34.5,22,16.5,15.625,14.625,14,16,18], + [90,48,32,21,16,15.5,14.5,14,16,18], + [95,45,28,19.5,15.5,15,14,13.5,16,18], + [100,42,26.5,18.5,15,14.5,13.5,13,16,18], + [105,39,25,18,14.5,14,13,12,16,18], + [110,36,24,18,14,13,12,11,16,18], + [115,34.5,22.5,17.25,12.5,11.875,11.25,10.625,14.5,16.5], + [120,33,21,16.5,11,10.75,10.5,10.25,13,15], + [125,32,20,16,10,10,10,10,12,14], + ]; + + +// ASCENDING SORTED Public Beta 0.1.7 (G) • MAY 28 2022 //// + +// DELTA - MAIN FONT LOOKUP May 28 2022 EXPANDED +// EXPANDED Sorted by Lc Value •• DELTA +// The pre-calculated deltas of the above array + +const fontDeltaAscend = [ + ['∆Lc',100,200,300,400,500,600,700,800,900], + [0,0,0,0,0,0,0,0,0,0], + [10,0,0,0,0,0,0,0,0,0], + [15,0,0,0,0,0,0,0,0,0], + [20,0,0,0,0,0,0,0,0,0], + [25,0,0,0,12,12,12,24,24,24], + [30,0,0,12,12,36,36,24,24,24], + [35,0,12,12,36,24,18,16,16,16], + [40,12,12,24,18,16,14,8,8,8], + [45,12,24,12,10,4,4,3,3,3], + [50,16,12,12,4,4,3,3,3,3], + [55,8,12,6,4,3,3,2,2,0], + [60,4,2,10,2.25,2,1,1,0,0], + [65,4,2,4,2.25,1,1,0.5,0,0], + [70,4,2,4,1.5,2,1,0.5,0,0], + [75,4,3.75,1,0.75,0.188,0.188,0,0,0], + [80,4,3.75,1,0.75,0.188,0.188,0,0,0], + [85,4,2.5,1,0.5,0.125,0.125,0,0,0], + [90,3,4,1.5,0.5,0.5,0.5,0.5,0,0], + [95,3,1.5,1,0.5,0.5,0.5,0.5,0,0], + [100,3,1.5,0.5,0.5,0.5,0.5,1,0,0], + [105,3,1,0,0.5,1,1,1,0,0], + [110,1.5,1.5,0.75,1.5,1.125,0.75,0.375,1.5,1.5], + [115,1.5,1.5,0.75,1.5,1.125,0.75,0.375,1.5,1.5], + [120,1,1,0.5,1,0.75,0.5,0.25,1,1], + [125,0,0,0,0,0,0,0,0,0], + ]; + + // APCA CONTRAST FONT LOOKUP TABLES + // Copyright © 2022 by Myndex Research and Andrew Somers. All Rights Reserved + // Public Beta 0.1.7 (G) • MAY 28 2022 + // For the following arrays, the Y axis is contrastArrayLen + // The two x axis are weightArrayLen and scoreArrayLen + + // MAY 28 2022 + + const weightArray = [0,100,200,300,400,500,600,700,800,900]; + const weightArrayLen = weightArray.length; // X axis + + let returnArray = [contrast.toFixed(places),0,0,0,0,0,0,0,0,0,]; + const returnArrayLen = returnArray.length; // X axis + + + const contrastArrayAscend = ['lc',0,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120,125,]; + const contrastArrayLenAsc = contrastArrayAscend.length; // Y azis + +//// Lc 45 * 0.2 = 9, and 9 is the index for the row for Lc 45 + + let tempFont = 777; + contrast = Math.abs(contrast); // Polarity unneeded for LUT + const factor = 0.2; // 1/5 as LUT is in increments of 5 + const index = (contrast == 0) ? + 1 : (contrast * factor) | 0 ; // LUT row... n|0 is bw floor + let w = 0; + // scoreAdj interpolates the needed font side per the Lc + let scoreAdj = (contrast - fontMatrixAscend[index][w]) * factor; + + w++; // determines column in font matrix LUT + + +///////// Font and Score Interpolation \///////////////////////////////// + +// populate returnArray with interpolated values + + for (; w < weightArrayLen; w++) { + + tempFont = fontMatrixAscend[index][w]; + + if (tempFont > 400) { // declares a specific minimum for the weight. + returnArray[w] = tempFont; + } else if (contrast < 14.5 ) { + returnArray[w] = 999; // 999 = do not use for anything + } else if (contrast < 29.5 ) { + returnArray[w] = 777; // 777 = non-text only + } else { + // INTERPOLATION OF FONT SIZE + // sets level for 0.5px size increments of smaller fonts + // Note bitwise (n|0) instead of floor + (tempFont > 24) ? + returnArray[w] = + Math.round(tempFont - (fontDeltaAscend[index][w] * scoreAdj)) : + returnArray[w] = + tempFont - ((2.0 * fontDeltaAscend[index][w] * scoreAdj) | 0) * 0.5; + // (n|0) is bitwise floor + } + } +///////// End Interpolation //////////////////////////////////////////// + + return returnArray +} // end fontLookupAPCA + +/////////\ ///////////////////////////\ +//////////\ END fontLookupAPCA() 0.1.7 (G) /////////////////////////////\ +/////////////////////////////////////////////////////////////////////////////\ + + + + +////////////////////////////////////////////////////////////////////////////// +////////// LUMINANCE CONVERTERS |////////////////////////////////////////// + + +////////// ƒ sRGBtoY() ////////////////////////////////////////////////// +export function sRGBtoY (rgb = [0,0,0]) { // send sRGB 8bpc (0xFFFFFF) or string + +// NOTE: Currently expects 0-255 + +///// APCA 0.0.98G - 4g - W3 Compatible Constants //////////////////// +/* +const mainTRC = 2.4; // 2.4 exponent emulates actual monitor perception + +const sRco = 0.2126729, + sGco = 0.7151522, + sBco = 0.0721750; // sRGB coefficients + */ +// Future: +// 0.2126478133913640 0.7151791475336150 0.0721730390750208 +// Derived from: +// xW yW K xR yR xG yG xB yB +// 0.312720 0.329030 6504 0.640 0.330 0.300 0.600 0.150 0.060 + + // linearize r, g, or b then apply coefficients + // and sum then return the resulting luminance + + function simpleExp (chan) { return Math.pow(chan/255.0, SA98G.mainTRC); }; + + return SA98G.sRco * simpleExp(rgb[0]) + + SA98G.sGco * simpleExp(rgb[1]) + + SA98G.sBco * simpleExp(rgb[2]); + +} // End sRGBtoY() + + + + +////////// ƒ displayP3toY() ///////////////////////////////////////////// +export function displayP3toY (rgb = [0,0,0]) { // send rgba array + +// NOTE: Currently Apple has the tuple as 0.0 to 1.0, NOT 255 + +///// APCA 0.0.98G - 4g - W3 Compatible Constants //////////////////// + +const mainTRC = 2.4; // 2.4 exponent emulates actual monitor perception + // Pending evaluation, because, Apple... + +const sRco = 0.2289829594805780, + sGco = 0.6917492625852380, + sBco = 0.0792677779341829; // displayP3 coefficients + +// Derived from: +// xW yW K xR yR xG yG xB yB +// 0.312720 0.329030 6504 0.680 0.320 0.265 0.690 0.150 0.060 + + // linearize r, g, or b then apply coefficients + // and sum then return the resulting luminance + + function simpleExp (chan) { return Math.pow(chan, mainTRC); }; + + return sRco * simpleExp(rgb[0]) + + sGco * simpleExp(rgb[1]) + + sBco * simpleExp(rgb[2]); + +} // End displayP3toY() + + + + +////////// ƒ adobeRGBtoY() ///////////////////////////////////////////// +export function adobeRGBtoY (rgb = [0,0,0]) { // send rgba array + +// NOTE: Currently expects 0-255 + +///// APCA 0.0.98G - 4g - W3 Compatible Constants //////////////////// + +const mainTRC = 2.35; // 2.35 exponent emulates actual monitor perception + // Pending evaluation... + +const sRco = 0.2973550227113810, + sGco = 0.6273727497145280, + sBco = 0.0752722275740913; // adobeRGB coefficients + +// Derived from: +// xW yW K xR yR xG yG xB yB +// 0.312720 0.329030 6504 0.640 0.330 0.210 0.710 0.150 0.060 + + // linearize r, g, or b then apply coefficients + // and sum then return the resulting luminance + + function simpleExp (chan) { return Math.pow(chan/255.0, mainTRC); }; + + return sRco * simpleExp(rgb[0]) + + sGco * simpleExp(rgb[1]) + + sBco * simpleExp(rgb[2]); + +} // End displayP3toY() + + + + +//////////////////////////////////////////////////////////////////////////// +////////// UTILITIES \/////////////////////////////////////////////////// + + +////////// ƒ alphaBlend() ///////////////////////////////////////////// + + // send rgba array for text/icon, rgb for background. + // Only foreground allows alpha of 0.0 to 1.0 + // This blends using gamma encoded space (standard) + // rounded 0-255 or set round=false for number 0.0-255.0 +export function alphaBlend (rgbaFG=[0,0,0,1.0], rgbBG=[0,0,0], round = true ) { + + rgbaFG[3] = Math.max(Math.min(rgbaFG[3], 1.0), 0.0); // clamp alpha 0-1 + let compBlend = 1.0 - rgbaFG[3]; + let rgbOut = [0,0,0,1,true]; // or just use rgbBG to retain other elements? + + for (let i=0;i<3;i++) { + rgbOut[i] = rgbBG[i] * compBlend + rgbaFG[i] * rgbaFG[3]; + if (round) rgbOut[i] = Math.min(Math.round(rgbOut[i]),255); + }; + return rgbOut; +} // End alphaBlend() + + + + +//\ //////////////////////////////////////// +///\ //////////////////////////////////////// +////\ //////////////////////////////////////// +/////\ END APCA 0.1.9 G-4g BLOCK //////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////// diff --git a/auto_contrast.js b/auto_contrast.js new file mode 100644 index 0000000..292aee1 --- /dev/null +++ b/auto_contrast.js @@ -0,0 +1,34 @@ +import {calcAPCA} from './apca-w3.js'; +import { formatHex, converter} from './culori.mjs'; +import {nearestNamedColors} from './color-picker.js'; + +export function find_contrast(col_a, col_b, target_contrast=90, num_close_colours=1, temp=0.1) { + const [bname] = nearestNamedColors(col_b,1); + let tcol_b = col_b; + function score(c) { + return Math.abs(c - target_contrast); + } + for(let j=0;j<30;j++) { + const possibles = []; + for(let i =0; i<50; i++) { + const {l, a, b} = tcol_b; + const [nl, na, nb] = [l,a,b].map(x=>x+(Math.random()*2-1)*temp); + const ncol = {mode: 'oklab', l: nl, a: na, b: nb}; + const names = nearestNamedColors(culori.oklab(formatHex(ncol)),num_close_colours); + if(!names.some(name => name==bname)) { + continue; + } + const contrast = Math.abs(calcAPCA(formatHex(col_a), formatHex(ncol))); + possibles.push({ncol, contrast}); + } + if(possibles.length==0) { + continue; + } + const [best] = possibles.sort((a,b) => {const ca = score(a.contrast); const cb = score(b.contrast); return ca { + const d = color_difference(a,b); + return d*Math.pow(COLOUR_WEIGHT,b.weight); +} + +const named_colours = colour_names.map(({name, hex, weight}) => { + const color = convertToLab(parseHex(hex)); + color.name = name; + color.weight = weight; + return color; +}) +export const nearestNamedColors = culori.nearest(named_colours, weighted_color_difference); + +export function name_colour(color) { + const closest = nearestNamedColors(color, 3); + return closest; +} + +export class ColorPickerElement extends HTMLElement { + + static observedAttributes = ['value']; + + constructor() { + super(); + let template = document.getElementById("color-picker-template"); + let templateContent = template.content; + + const sheet = new CSSStyleSheet(); + sheet.replaceSync(` + * {box-sizing: border-box;} + + .color-input { + display: grid; + grid-gap: 0.5em; + grid-template-columns: 1fr 1fr; + } + .color-input label { + grid-column: 1; + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + grid-gap: 1em; + } + .color-input input { + width: 100%; + } + + .color-input .color-preview { + display: block; + width: 100%; + height: 100%; + grid-column: 2; + grid-row: 1/5; + background: red; + padding: 1em; + } + + .color-input span { + background: white; + padding: 0.2em; + display: inline-block; + } + + .color-input .names { + list-style: none; + padding: 0; + } + + `); + + const shadowRoot = this.attachShadow({ mode: "open" }); + shadowRoot.appendChild(templateContent.cloneNode(true)); + shadowRoot.adoptedStyleSheets = [sheet]; + + const colour_name_datalist = shadowRoot.querySelector('#colour-names'); + named_colours.forEach(({name}) => { + const option = document.createElement('option'); + option.value = option.textContent = name; + colour_name_datalist.append(option); + }) + + this.name_input = shadowRoot.querySelector('input[name="name"]'); + + Array.from(shadowRoot.querySelectorAll('.color-input input[type="range"]')).forEach(i => i.addEventListener('input', () => { + this.name_input.value = ''; + this.update(); + })); + + this.inputs = Object.fromEntries(Array.from(this.shadowRoot.querySelector('.color-input').elements).map(e=>[e.name, e])); + + this.name_input.addEventListener('input', e => { + this.value = e.target.value.trim(); + }); + + this.value = this.getAttribute('value'); + + this.update(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if(name == 'value') { + this.value = newValue; + this.name_input.value = ''; + } + } + + set value(value) { + const col = culori.parse(value); + if(col) { + this.set_inputs(col); + this.update(); + } + } + + set_inputs(col) { + const okcol = convertToOklab(col); + this.inputs.L.value = okcol.l; + this.inputs.a.value = okcol.a; + this.inputs.b.value = okcol.b; + } + + get value() { + const values = Object.fromEntries(Array.from(this.shadowRoot.querySelector('.color-input').elements).map(e=>[e.name, e.valueAsNumber])); + const {L, a, b} = values; + const col = {mode: 'oklab', l: L, a: a, b: b}; + return col; + } + + update() { + const col = this.value; + const hex = formatHex(col); + const closest = name_colour(col); + + this.shadowRoot.querySelector('output').style.background = formatCss(col); + this.shadowRoot.querySelector('.hex').textContent = hex; + Array.from(this.shadowRoot.querySelectorAll('.names li span')).forEach((e,i) => { + e.textContent = closest[i].name; + }) + + this.dispatchEvent(new CustomEvent('input',{detail: col})); + } +} + +customElements.define( + "color-picker", + ColorPickerElement +); diff --git a/colour_names.js b/colour_names.js new file mode 100644 index 0000000..1d7c48e --- /dev/null +++ b/colour_names.js @@ -0,0 +1,30 @@ +// colour names drawn from https://simple.wikipedia.org/wiki/Colour + +const colours = ` +red: #FE2712: 1 +orange: #FF7F00: 1 +yellow: #FEFE33: 1 +green: #008000: 1 +blue: #0247FE: 1 +cyan: #00ffff: 2 +purple: #A020F0: 2 +white: #ffffff: 1 +black: #000000: 1 +brown: #964B00: 2 +magenta: #ff00ff: 2 +tan: #D2B48C: 3 +olive: #808000: 3 +maroon: #800000: 3 +navy: #000080: 3 +aquamarine: #7FFFD4: 4 +turquoise: #40e0d0: 4 +silver: #C0C0C0: 4 +lime: #BFFF00: 4 +teal: #008080: 4 +indigo: #4b0082: 4 +violet: #8601AF: 2 +pink: #FF69B4: 1 +grey: #808080: 2 +`.trim().split('\n').map(l=>{const [name, hex, weight] = l.split(': '); return {name, hex, weight: parseFloat(weight)}}); + +export default colours; \ No newline at end of file diff --git a/convert.js b/convert.js new file mode 100644 index 0000000..8d8c3f3 --- /dev/null +++ b/convert.js @@ -0,0 +1,219 @@ +/** From culori.js + * + */ + +var k = Math.pow(29, 3) / Math.pow(3, 3); +var e = Math.pow(6, 3) / Math.pow(29, 3); + +var D65 = { + X: 0.3127 / 0.329, + Y: 1, + Z: (1 - 0.3127 - 0.329) / 0.329 +}; + +var f = (value) => value > e ? Math.cbrt(value) : (k * value + 16) / 116; +var convertXyz65ToLab65 = ({ x, y, z, alpha }) => { + let f0 = f(x / D65.X); + let f1 = f(y / D65.Y); + let f22 = f(z / D65.Z); + let res = { + mode: "lab65", + l: 116 * f1 - 16, + a: 500 * (f0 - f1), + b: 200 * (f1 - f22) + }; + if (alpha !== void 0) { + res.alpha = alpha; + } + return res; +}; +var convertXyz65ToLab65_default = convertXyz65ToLab65; + +// src/xyz65/convertRgbToXyz65.js +var convertRgbToXyz65 = (rgb4) => { + let { r: r2, g, b, alpha } = convertRgbToLrgb_default(rgb4); + let res = { + mode: "xyz65", + x: 0.4123907992659593 * r2 + 0.357584339383878 * g + 0.1804807884018343 * b, + y: 0.2126390058715102 * r2 + 0.715168678767756 * g + 0.0721923153607337 * b, + z: 0.0193308187155918 * r2 + 0.119194779794626 * g + 0.9505321522496607 * b + }; + if (alpha !== void 0) { + res.alpha = alpha; + } + return res; +}; +var convertRgbToXyz65_default = convertRgbToXyz65; + +// src/lab65/convertRgbToLab65.js +var convertRgbToLab65 = (rgb4) => { + let res = convertXyz65ToLab65_default(convertRgbToXyz65_default(rgb4)); + if (rgb4.r === rgb4.b && rgb4.b === rgb4.g) { + res.a = res.b = 0; + } + return res; +}; +var convertRgbToLab65_default = convertRgbToLab65; + + +var convertLrgbToOklab = ({ r: r2, g, b, alpha }) => { + let L = Math.cbrt( + 0.41222147079999993 * r2 + 0.5363325363 * g + 0.0514459929 * b + ); + let M2 = Math.cbrt( + 0.2119034981999999 * r2 + 0.6806995450999999 * g + 0.1073969566 * b + ); + let S = Math.cbrt( + 0.08830246189999998 * r2 + 0.2817188376 * g + 0.6299787005000002 * b + ); + let res = { + mode: "oklab", + l: 0.2104542553 * L + 0.793617785 * M2 - 0.0040720468 * S, + a: 1.9779984951 * L - 2.428592205 * M2 + 0.4505937099 * S, + b: 0.0259040371 * L + 0.7827717662 * M2 - 0.808675766 * S + }; + if (alpha !== void 0) { + res.alpha = alpha; + } + return res; +}; +var convertLrgbToOklab_default = convertLrgbToOklab; + +// src/lrgb/convertRgbToLrgb.js +var fn = (c4) => { + const abs3 = Math.abs(c4); + if (abs3 <= 0.04045) { + return c4 / 12.92; + } + return (Math.sign(c4) || 1) * Math.pow((abs3 + 0.055) / 1.055, 2.4); +}; +var convertRgbToLrgb = ({ r: r2, g, b, alpha }) => { + let res = { + mode: "lrgb", + r: fn(r2), + g: fn(g), + b: fn(b) + }; + if (alpha !== void 0) + res.alpha = alpha; + return res; +}; +var convertRgbToLrgb_default = convertRgbToLrgb; + +// src/oklab/convertRgbToOklab.js +var convertRgbToOklab = (rgb4) => { + let res = convertLrgbToOklab_default(convertRgbToLrgb_default(rgb4)); + if (rgb4.r === rgb4.b && rgb4.b === rgb4.g) { + res.a = res.b = 0; + } + return res; +}; +var convertRgbToOklab_default = convertRgbToOklab; + +// src/oklab/convertOklabToLrgb.js +var convertOklabToLrgb = ({ l, a, b, alpha }) => { + let L = Math.pow( + l * 0.9999999984505198 + 0.39633779217376786 * a + 0.2158037580607588 * b, + 3 + ); + let M2 = Math.pow( + l * 1.0000000088817609 - 0.10556134232365635 * a - 0.06385417477170591 * b, + 3 + ); + let S = Math.pow( + l * 1.0000000546724108 - 0.08948418209496575 * a - 1.2914855378640917 * b, + 3 + ); + let res = { + mode: "lrgb", + r: 4.076741661347994 * L - 3.307711590408193 * M2 + 0.230969928729428 * S, + g: -1.2684380040921763 * L + 2.6097574006633715 * M2 - 0.3413193963102197 * S, + b: -0.004196086541837188 * L - 0.7034186144594493 * M2 + 1.7076147009309444 * S + }; + if (alpha !== void 0) { + res.alpha = alpha; + } + return res; +}; +var convertOklabToLrgb_default = convertOklabToLrgb; + +// src/oklab/convertOklabToRgb.js +var convertOklabToRgb = (c4) => convertLrgbToRgb_default(convertOklabToLrgb_default(c4)); +var convertOklabToRgb_default = convertOklabToRgb; + +var convertLabToLch = ({ l, a, b, alpha }, mode = "lch") => { + let c4 = Math.sqrt(a * a + b * b); + let res = { mode, l, c: c4 }; + if (c4) + res.h = normalizeHue_default(Math.atan2(b, a) * 180 / Math.PI); + if (alpha !== void 0) + res.alpha = alpha; + return res; +}; + +// src/util/normalizeHue.js +var normalizeHue = (hue3) => (hue3 = hue3 % 360) < 0 ? hue3 + 360 : hue3; +var normalizeHue_default = normalizeHue; + +// src/lch/convertLabToLch.js +var convertLabToLch = ({ l, a, b, alpha }, mode = "lch") => { + let c4 = Math.sqrt(a * a + b * b); + let res = { mode, l, c: c4 }; + if (c4) + res.h = normalizeHue_default(Math.atan2(b, a) * 180 / Math.PI); + if (alpha !== void 0) + res.alpha = alpha; + return res; +}; +var convertLabToLch_default = convertLabToLch; + +// src/lch/convertLchToLab.js +function convertLchToLab({ l, c: c4, h, alpha }, mode = "lab") { + let res = { + mode, + l, + a: c4 ? c4 * Math.cos(h / 180 * Math.PI) : 0, + b: c4 ? c4 * Math.sin(h / 180 * Math.PI) : 0 + }; + if (alpha !== void 0) + res.alpha = alpha; + return res; +}; +var convertLchToLab_default = convertLchToLab; + +function convertRgbToOklch(col) { + return convertLabToLch(convertRgbToOklab(col)); +} + +function convertOklchToRgb(col) { + return convertLabToRgb(convertLchToLab(col)); +} + +function lerp(a, b, t) { + if(a===undefined) { + return b; + } + if(b===undefined) { + return a; + } + return (1-t)*a + t*b; +} + +/** By Christian Lawson-Perfect + */ + +function parseRGB(hex) { + var r = parseInt(hex.slice(1,3),16); + var g = parseInt(hex.slice(3,5),16); + var b = parseInt(hex.slice(5,7),16); + return {r: r/255, g: g/255, b: b/255, mode: 'rgb'} ; +} + +function mix(oklab1, oklab2, t) { + const {l: l1, c: c1, h: h1} = oklab1; + const {l: l2, c: c2, h: h2} = oklab2; + + return {l: lerp(l1,l2,t), c: lerp(c1,c2,t), h: lerp(h1,h2,t), mode: 'oklch'}; +} + +export {parseRGB, convertOklchToRgb, convertRgbToOklch, convertRgbToLab65, mix}; diff --git a/culori.mjs b/culori.mjs new file mode 100644 index 0000000..967747b --- /dev/null +++ b/culori.mjs @@ -0,0 +1,4 @@ +var dr=(e,t)=>{if(typeof e=="number"){if(t===3)return{mode:"rgb",r:(e>>8&15|e>>4&240)/255,g:(e>>4&15|e&240)/255,b:(e&15|e<<4&240)/255};if(t===4)return{mode:"rgb",r:(e>>12&15|e>>8&240)/255,g:(e>>8&15|e>>4&240)/255,b:(e>>4&15|e&240)/255,alpha:(e&15|e<<4&240)/255};if(t===6)return{mode:"rgb",r:(e>>16&255)/255,g:(e>>8&255)/255,b:(e&255)/255};if(t===8)return{mode:"rgb",r:(e>>24&255)/255,g:(e>>16&255)/255,b:(e>>8&255)/255,alpha:(e&255)/255}}},ae=dr;var lr={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},ut=lr;var pr=e=>ae(ut[e.toLowerCase()],6),mt=pr;var ur=/^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i,mr=e=>{let t;return(t=e.match(ur))?ae(parseInt(t[1],16),t[1].length):void 0},fe=mr;var c="([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)",Ko=`(?:${c}|none)`,N=`${c}%`,Qo=`(?:${c}%|none)`,W=`(?:${c}%|${c})`,sr=`(?:${c}%|${c}|none)`,st=`(?:${c}(deg|grad|rad|turn)|${c})`,Uo=`(?:${c}(deg|grad|rad|turn)|${c}|none)`,H="\\s*,\\s*";var Vo=new RegExp("^"+sr+"$");var cr=new RegExp(`^rgba?\\(\\s*${c}${H}${c}${H}${c}\\s*(?:,\\s*${W}\\s*)?\\)$`),br=new RegExp(`^rgba?\\(\\s*${N}${H}${N}${H}${N}\\s*(?:,\\s*${W}\\s*)?\\)$`),hr=e=>{let t={mode:"rgb"},r;if(r=e.match(cr))r[1]!==void 0&&(t.r=r[1]/255),r[2]!==void 0&&(t.g=r[2]/255),r[3]!==void 0&&(t.b=r[3]/255);else if(r=e.match(br))r[1]!==void 0&&(t.r=r[1]/100),r[2]!==void 0&&(t.g=r[2]/100),r[3]!==void 0&&(t.b=r[3]/100);else return;return r[4]!==void 0?t.alpha=Math.max(0,Math.min(1,r[4]/100)):r[5]!==void 0&&(t.alpha=Math.max(0,Math.min(1,+r[5]))),t},ct=hr;var xr=(e,t)=>e===void 0?void 0:typeof e!="object"?de(e):e.mode!==void 0?e:t?{...e,mode:t}:void 0,ie=xr;var gr=(e="rgb")=>t=>(t=ie(t,e))!==void 0?t.mode===e?t:g[t.mode][e]?g[t.mode][e](t):e==="rgb"?g[t.mode].rgb(t):g.rgb[e](g[t.mode].rgb(t)):void 0,h=gr;var g={},bt={},K=[],Re={},vr=e=>e,u=e=>(g[e.mode]={...g[e.mode],...e.toMode},Object.keys(e.fromMode||{}).forEach(t=>{g[t]||(g[t]={}),g[t][e.mode]=e.fromMode[t]}),e.ranges||(e.ranges={}),e.difference||(e.difference={}),e.channels.forEach(t=>{if(e.ranges[t]===void 0&&(e.ranges[t]=[0,1]),!e.interpolate[t])throw new Error(`Missing interpolator for: ${t}`);typeof e.interpolate[t]=="function"&&(e.interpolate[t]={use:e.interpolate[t]}),e.interpolate[t].fixup||(e.interpolate[t].fixup=vr)}),bt[e.mode]=e,(e.parse||[]).forEach(t=>{yr(t,e.mode)}),h(e.mode)),_=e=>bt[e],yr=(e,t)=>{if(typeof e=="string"){if(!t)throw new Error("'mode' required when 'parser' is a string");Re[e]=t}else typeof e=="function"&&K.indexOf(e)<0&&K.push(e)};var He=/[^\x00-\x7F]|[a-zA-Z_]/,Mr=/[^\x00-\x7F]|[-\w]/,f={Function:"function",Ident:"ident",Number:"number",Percentage:"percentage",ParenClose:")",None:"none",Hue:"hue",Alpha:"alpha"},d=0;function le(e){let t=e[d],r=e[d+1];return t==="-"||t==="+"?/\d/.test(r)||r==="."&&/\d/.test(e[d+2]):t==="."?/\d/.test(r):/\d/.test(t)}function ke(e){if(d>=e.length)return!1;let t=e[d];if(He.test(t))return!0;if(t==="-"){if(e.length-d<2)return!1;let r=e[d+1];return!!(r==="-"||He.test(r))}return!1}var Tr={deg:1,rad:180/Math.PI,grad:9/10,turn:360};function Q(e){let t="";if((e[d]==="-"||e[d]==="+")&&(t+=e[d++]),t+=pe(e),e[d]==="."&&/\d/.test(e[d+1])&&(t+=e[d++]+pe(e)),(e[d]==="e"||e[d]==="E")&&((e[d+1]==="-"||e[d+1]==="+")&&/\d/.test(e[d+2])?t+=e[d++]+e[d++]+pe(e):/\d/.test(e[d+1])&&(t+=e[d++]+pe(e))),ke(e)){let r=ue(e);return r==="deg"||r==="rad"||r==="turn"||r==="grad"?{type:f.Hue,value:t*Tr[r]}:void 0}return e[d]==="%"?(d++,{type:f.Percentage,value:+t}):{type:f.Number,value:+t}}function pe(e){let t="";for(;/\d/.test(e[d]);)t+=e[d++];return t}function ue(e){let t="";for(;d4)){if(r.length===4){if(r[3].type!==f.Alpha)return;r[3]=r[3].value}return r.length===3&&r.push({type:f.None,value:void 0}),r.every(n=>n.type!==f.Alpha)?r:void 0}}function Rr(e,t){e._i=0;let r=e[e._i++];if(!r||r.type!==f.Function)return;let o=ht(e,t);if(o)return o.unshift(r.value),o}var Hr=e=>{if(typeof e!="string")return;let t=Lr(e),r=t?Rr(t,!0):void 0,o,n=0,a=K.length;for(;ne==="transparent"?{mode:"rgb",r:0,g:0,b:0,alpha:0}:void 0,gt=Xr;var vt=(e,t,r)=>e+r*(t-e);var $r=e=>{let t=[];for(let r=0;rt=>{let r=$r(t);return o=>{let n=o*r.length,a=o>=1?r.length-1:Math.max(Math.floor(n),0),i=r[a];return i===void 0?void 0:e(i[0],i[1],n-a)}};var l=yt(vt);var m=e=>{let t=!1,r=e.map(o=>o!==void 0?(t=!0,o):1);return t?r:e};var Sr={mode:"rgb",channels:["r","g","b","alpha"],parse:[xt,fe,ct,mt,gt,"srgb"],serialize:"srgb",interpolate:{r:l,g:l,b:l,alpha:{use:l,fixup:m}},gamut:!0,white:{r:1,g:1,b:1},black:{r:0,g:0,b:0}},x=Sr;var Xe=(e=0)=>Math.pow(Math.abs(e),2.19921875)*Math.sign(e),Nr=e=>{let t=Xe(e.r),r=Xe(e.g),o=Xe(e.b),n={mode:"xyz65",x:.5766690429101305*t+.1855582379065463*r+.1882286462349947*o,y:.297344975250536*t+.6273635662554661*r+.0752914584939979*o,z:.0270313613864123*t+.0706888525358272*r+.9913375368376386*o};return e.alpha!==void 0&&(n.alpha=e.alpha),n},$e=Nr;var Se=e=>Math.pow(Math.abs(e),.4547069271758437)*Math.sign(e),Pr=({x:e,y:t,z:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n={mode:"a98",r:Se(e*2.0415879038107465-t*.5650069742788597-.3447313507783297*r),g:Se(e*-.9692436362808798+t*1.8759675015077206+.0415550574071756*r),b:Se(e*.0134442806320312-t*.1183623922310184+1.0151749943912058*r)};return o!==void 0&&(n.alpha=o),n},Ne=Pr;var Pe=(e=0)=>{let t=Math.abs(e);return t<=.04045?e/12.92:(Math.sign(e)||1)*Math.pow((t+.055)/1.055,2.4)},Ir=({r:e,g:t,b:r,alpha:o})=>{let n={mode:"lrgb",r:Pe(e),g:Pe(t),b:Pe(r)};return o!==void 0&&(n.alpha=o),n},y=Ir;var Or=e=>{let{r:t,g:r,b:o,alpha:n}=y(e),a={mode:"xyz65",x:.4123907992659593*t+.357584339383878*r+.1804807884018343*o,y:.2126390058715102*t+.715168678767756*r+.0721923153607337*o,z:.0193308187155918*t+.119194779794626*r+.9505321522496607*o};return n!==void 0&&(a.alpha=n),a},M=Or;var Ie=(e=0)=>{let t=Math.abs(e);return t>.0031308?(Math.sign(e)||1)*(1.055*Math.pow(t,.4166666666666667)-.055):e*12.92},Cr=({r:e,g:t,b:r,alpha:o},n="rgb")=>{let a={mode:n,r:Ie(e),g:Ie(t),b:Ie(r)};return o!==void 0&&(a.alpha=o),a},T=Cr;var Ar=({x:e,y:t,z:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=T({r:e*3.2409699419045226-t*1.537383177570094-.4986107602930034*r,g:e*-.9692436362808796+t*1.8759675015077204+.0415550574071756*r,b:e*.0556300796969936-t*.2039769588889765+1.0569715142428784*r});return o!==void 0&&(n.alpha=o),n},z=Ar;var qr={...x,mode:"a98",parse:["a98-rgb"],serialize:"a98-rgb",fromMode:{rgb:e=>Ne(M(e)),xyz65:Ne},toMode:{rgb:e=>z($e(e)),xyz65:$e}},Mt=qr;var _r=e=>(e=e%360)<0?e+360:e,s=_r;function Oe({h:e,s:t,l:r,alpha:o}){e=s(e!==void 0?e:0),t===void 0&&(t=0),r===void 0&&(r=0);let n=r+t*(r<.5?r:1-r),a=n-(n-r)*2*Math.abs(e/60%2-1),i;switch(Math.floor(e/60)){case 0:i={r:n,g:a,b:2*r-n};break;case 1:i={r:a,g:n,b:2*r-n};break;case 2:i={r:2*r-n,g:n,b:a};break;case 3:i={r:2*r-n,g:a,b:n};break;case 4:i={r:a,g:2*r-n,b:n};break;case 5:i={r:n,g:2*r-n,b:a};break;default:i={r:2*r-n,g:2*r-n,b:2*r-n}}return i.mode="rgb",o!==void 0&&(i.alpha=o),i}function Ce({r:e,g:t,b:r,alpha:o}){e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=Math.max(e,t,r),a=Math.min(e,t,r),i={mode:"hsl",s:n===a?0:(n-a)/(1-Math.abs(n+a-1)),l:.5*(n+a)};return n-a!==0&&(i.h=(n===e?(t-r)/(n-a)+(t{switch(t){case"deg":return+e;case"rad":return e/Math.PI*180;case"grad":return e/10*9;case"turn":return e*360}},Tt=Dr;var Er=new RegExp(`^hsla?\\(\\s*${st}${H}${N}${H}${N}\\s*(?:,\\s*${W}\\s*)?\\)$`),Jr=e=>{let t=e.match(Er);if(!t)return;let r={mode:"hsl"};return t[3]!==void 0?r.h=+t[3]:t[1]!==void 0&&t[2]!==void 0&&(r.h=Tt(t[1],t[2])),t[4]!==void 0&&(r.s=Math.min(Math.max(0,t[4]/100),1)),t[5]!==void 0&&(r.l=Math.min(Math.max(0,t[5]/100),1)),t[6]!==void 0?r.alpha=Math.max(0,Math.min(1,t[6]/100)):t[7]!==void 0&&(r.alpha=Math.max(0,Math.min(1,+t[7]))),r},zt=Jr;function Yr(e,t){if(!t||t[0]!=="hsl"&&t[0]!=="hsla")return;let r={mode:"hsl"},[,o,n,a,i]=t;if(o.type!==f.None){if(o.type===f.Percentage)return;r.h=o.value}if(n.type!==f.None){if(n.type===f.Hue)return;r.s=n.value/100}if(a.type!==f.None){if(a.type===f.Hue)return;r.l=a.value/100}return i.type!==f.None&&(r.alpha=Math.min(1,Math.max(0,i.type===f.Number?i.value:i.value/100))),r}var Lt=Yr;var Fr=(e,t)=>e.map((r,o,n)=>{if(r===void 0)return r;let a=s(r);return o===0||e[o-1]===void 0?a:t(a-s(n[o-1]))}).reduce((r,o)=>!r.length||o===void 0||r[r.length-1]===void 0?(r.push(o),r):(r.push(o+r[r.length-1]),r),[]),k=e=>Fr(e,t=>Math.abs(t)<=180?t:t-360*Math.sign(t));var U=(e,t)=>{if(e.h===void 0||t.h===void 0||!e.s||!t.s)return 0;let r=s(e.h),o=s(t.h),n=Math.sin((o-r+360)/2*Math.PI/180);return 2*Math.sqrt(e.s*t.s)*n},Ae=(e,t)=>{if(e.h===void 0||t.h===void 0)return 0;let r=s(e.h),o=s(t.h);return Math.abs(o-r)>180?r-(o-360*Math.sign(o-r)):o-r},qe=(e,t)=>{if(e.h===void 0||t.h===void 0||!e.c||!t.c)return 0;let r=s(e.h),o=s(t.h),n=Math.sin((o-r+360)/2*Math.PI/180);return 2*Math.sqrt(e.c*t.c)*n},_e=(e="rgb",t=[1,1,1,0])=>{let r=_(e),o=r.channels,n=r.difference,a=h(e);return(i,p)=>{let b=a(i),P=a(p);return Math.sqrt(o.reduce((R,I,te)=>{let O=n[I]?n[I](b,P):b[I]-P[I];return R+(t[te]||0)*Math.pow(isNaN(O)?0:O,2)},0))}};var wt=(e=1,t=1,r=1)=>{let o=h("lab65");return(n,a)=>{let i=o(n),p=o(a),b=i.l,P=i.a,R=i.b,I=Math.sqrt(P*P+R*R),te=p.l,O=p.a,C=p.b,er=Math.sqrt(O*O+C*C),nt=(I+er)/2,at=.5*(1-Math.sqrt(Math.pow(nt,7)/(Math.pow(nt,7)+Math.pow(25,7)))),re=P*(1+at),oe=O*(1+at),Z=Math.sqrt(re*re+R*R),B=Math.sqrt(oe*oe+C*C),A=Math.abs(re)+Math.abs(R)===0?0:Math.atan2(R,re);A+=(A<0)*2*Math.PI;let q=Math.abs(oe)+Math.abs(C)===0?0:Math.atan2(C,oe);q+=(q<0)*2*Math.PI;let tr=te-b,ft=B-Z,j=Z*B===0?0:q-A;j-=(j>Math.PI)*2*Math.PI,j+=(j<-Math.PI)*2*Math.PI;let it=2*Math.sqrt(Z*B)*Math.sin(j/2),rr=(b+te)/2,ne=(Z+B)/2,v;Z*B===0?v=A+q:(v=(A+q)/2,v-=(Math.abs(A-q)>Math.PI)*Math.PI,v+=(v<0)*2*Math.PI);let dt=Math.pow(rr-50,2),or=1-.17*Math.cos(v-Math.PI/6)+.24*Math.cos(2*v)+.32*Math.cos(3*v+Math.PI/30)-.2*Math.cos(4*v-63*Math.PI/180),nr=1+.015*dt/Math.sqrt(20+dt),lt=1+.045*ne,pt=1+.015*ne*or,ar=30*Math.PI/180*Math.exp(-1*Math.pow((180/Math.PI*v-275)/25,2)),fr=2*Math.sqrt(Math.pow(ne,7)/(Math.pow(ne,7)+Math.pow(25,7))),ir=-1*Math.sin(2*ar)*fr;return Math.sqrt(Math.pow(tr/(e*nr),2)+Math.pow(ft/(t*lt),2)+Math.pow(it/(r*pt),2)+ir*ft/(t*lt)*it/(r*pt))}};var X=e=>{let t=e.reduce((o,n)=>{if(n!==void 0){let a=n*Math.PI/180;o.sin+=Math.sin(a),o.cos+=Math.cos(a)}return o},{sin:0,cos:0}),r=Math.atan2(t.sin,t.cos)*180/Math.PI;return r<0?360+r:r};var Gr={mode:"hsl",toMode:{rgb:Oe},fromMode:{rgb:Ce},channels:["h","s","l","alpha"],ranges:{h:[0,360]},gamut:"rgb",parse:[Lt,zt],serialize:e=>`hsl(${e.h!==void 0?e.h:"none"} ${e.s!==void 0?e.s*100+"%":"none"} ${e.l!==void 0?e.l*100+"%":"none"}${e.alpha<1?` / ${e.alpha}`:""})`,interpolate:{h:{use:l,fixup:k},s:l,l,alpha:{use:l,fixup:m}},difference:{h:U},average:{h:X}},Rt=Gr;function V({h:e,s:t,v:r,alpha:o}){e=s(e!==void 0?e:0),t===void 0&&(t=0),r===void 0&&(r=0);let n=Math.abs(e/60%2-1),a;switch(Math.floor(e/60)){case 0:a={r,g:r*(1-t*n),b:r*(1-t)};break;case 1:a={r:r*(1-t*n),g:r,b:r*(1-t)};break;case 2:a={r:r*(1-t),g:r,b:r*(1-t*n)};break;case 3:a={r:r*(1-t),g:r*(1-t*n),b:r};break;case 4:a={r:r*(1-t*n),g:r*(1-t),b:r};break;case 5:a={r,g:r*(1-t),b:r*(1-t*n)};break;default:a={r:r*(1-t),g:r*(1-t),b:r*(1-t)}}return a.mode="rgb",o!==void 0&&(a.alpha=o),a}function ee({r:e,g:t,b:r,alpha:o}){e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=Math.max(e,t,r),a=Math.min(e,t,r),i={mode:"hsv",s:n===0?0:1-a/n,v:n};return n-a!==0&&(i.h=(n===e?(t-r)/(n-a)+(t1){let n=t+r;t/=n,r/=n}return V({h:e,s:r===1?1:1-t/(1-r),v:1-r,alpha:o})}function Ee(e){let t=ee(e);if(t===void 0)return;let r=t.s!==void 0?t.s:0,o=t.v!==void 0?t.v:0,n={mode:"hwb",w:(1-r)*o,b:1-o};return t.h!==void 0&&(n.h=t.h),t.alpha!==void 0&&(n.alpha=t.alpha),n}function Br(e,t){if(!t||t[0]!=="hwb")return;let r={mode:"hwb"},[,o,n,a,i]=t;if(o.type!==f.None){if(o.type===f.Percentage)return;r.h=o.value}if(n.type!==f.None){if(n.type===f.Hue)return;r.w=n.value/100}if(a.type!==f.None){if(a.type===f.Hue)return;r.b=a.value/100}return i.type!==f.None&&(r.alpha=Math.min(1,Math.max(0,i.type===f.Number?i.value:i.value/100))),r}var kt=Br;var jr={mode:"hwb",toMode:{rgb:De},fromMode:{rgb:Ee},channels:["h","w","b","alpha"],ranges:{h:[0,360]},gamut:"rgb",parse:[kt],serialize:e=>`hwb(${e.h!==void 0?e.h:"none"} ${e.w!==void 0?e.w*100+"%":"none"} ${e.b!==void 0?e.b*100+"%":"none"}${e.alpha<1?` / ${e.alpha}`:""})`,interpolate:{h:{use:l,fixup:k},w:l,b:l,alpha:{use:l,fixup:m}},difference:{h:Ae},average:{h:X}},Xt=jr;var me=Math.pow(29,3)/Math.pow(3,3),se=Math.pow(6,3)/Math.pow(29,3);var $={X:.9642956764295677,Y:1,Z:.8251046025104602},S={X:.3127/.329,Y:1,Z:(1-.3127-.329)/.329},Fa=Math.pow(29,3)/Math.pow(3,3),Ga=Math.pow(6,3)/Math.pow(29,3);var Je=e=>Math.pow(e,3)>se?Math.pow(e,3):(116*e-16)/me,Wr=({l:e,a:t,b:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=(e+16)/116,a=t/500+n,i=n-r/200,p={mode:"xyz50",x:Je(a)*$.X,y:Je(n)*$.Y,z:Je(i)*$.Z};return o!==void 0&&(p.alpha=o),p},D=Wr;var Kr=({x:e,y:t,z:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=T({r:e*3.1341359569958707-t*1.6173863321612538-.4906619460083532*r,g:e*-.978795502912089+t*1.916254567259524+.03344273116131949*r,b:e*.07195537988411677-t*.2289768264158322+1.405386058324125*r});return o!==void 0&&(n.alpha=o),n},E=Kr;var Qr=e=>E(D(e)),ce=Qr;var Ur=e=>{let{r:t,g:r,b:o,alpha:n}=y(e),a={mode:"xyz50",x:.436065742824811*t+.3851514688337912*r+.14307845442264197*o,y:.22249319175623702*t+.7168870538238823*r+.06061979053616537*o,z:.013923904500943465*t+.09708128566574634*r+.7140993584005155*o};return n!==void 0&&(a.alpha=n),a},J=Ur;var Ye=e=>e>se?Math.cbrt(e):(me*e+16)/116,Vr=({x:e,y:t,z:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=Ye(e/$.X),a=Ye(t/$.Y),i=Ye(r/$.Z),p={mode:"lab",l:116*a-16,a:500*(n-a),b:200*(a-i)};return o!==void 0&&(p.alpha=o),p},Y=Vr;var eo=e=>{let t=Y(J(e));return e.r===e.b&&e.b===e.g&&(t.a=t.b=0),t},be=eo;function to(e,t){if(!t||t[0]!=="lab")return;let r={mode:"lab"},[,o,n,a,i]=t;if(!(o.type===f.Hue||n.type===f.Hue||a.type===f.Hue))return o.type!==f.None&&(r.l=Math.min(Math.max(0,o.value),100)),n.type!==f.None&&(r.a=n.type===f.Number?n.value:n.value*125/100),a.type!==f.None&&(r.b=a.type===f.Number?a.value:a.value*125/100),i.type!==f.None&&(r.alpha=Math.min(1,Math.max(0,i.type===f.Number?i.value:i.value/100))),r}var $t=to;var ro={mode:"lab",toMode:{xyz50:D,rgb:ce},fromMode:{xyz50:Y,rgb:be},channels:["l","a","b","alpha"],ranges:{l:[0,100],a:[-100,100],b:[-100,100]},parse:[$t],serialize:e=>`lab(${e.l!==void 0?e.l:"none"} ${e.a!==void 0?e.a:"none"} ${e.b!==void 0?e.b:"none"}${e.alpha<1?` / ${e.alpha}`:""})`,interpolate:{l,a:l,b:l,alpha:{use:l,fixup:m}}},F=ro;var he=Math.pow(29,3)/Math.pow(3,3),xe=Math.pow(6,3)/Math.pow(29,3);var Fe=e=>Math.pow(e,3)>xe?Math.pow(e,3):(116*e-16)/he,oo=({l:e,a:t,b:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=(e+16)/116,a=t/500+n,i=n-r/200,p={mode:"xyz65",x:Fe(a)*S.X,y:Fe(n)*S.Y,z:Fe(i)*S.Z};return o!==void 0&&(p.alpha=o),p},ge=oo;var no=e=>z(ge(e)),ve=no;var Ge=e=>e>xe?Math.cbrt(e):(he*e+16)/116,ao=({x:e,y:t,z:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=Ge(e/S.X),a=Ge(t/S.Y),i=Ge(r/S.Z),p={mode:"lab65",l:116*a-16,a:500*(n-a),b:200*(a-i)};return o!==void 0&&(p.alpha=o),p},ye=ao;var fo=e=>{let t=ye(M(e));return e.r===e.b&&e.b===e.g&&(t.a=t.b=0),t},Me=fo;var io={...F,mode:"lab65",parse:["--lab-d65"],serialize:"--lab-d65",toMode:{xyz65:ge,rgb:ve},fromMode:{xyz65:ye,rgb:Me},ranges:{l:[0,100],a:[-86.182,98.234],b:[-107.86,94.477]}},St=io;var lo=({l:e,a:t,b:r,alpha:o},n="lch")=>{t===void 0&&(t=0),r===void 0&&(r=0);let a=Math.sqrt(t*t+r*r),i={mode:n,l:e,c:a};return a&&(i.h=s(Math.atan2(r,t)*180/Math.PI)),o!==void 0&&(i.alpha=o),i},L=lo;var po=({l:e,c:t,h:r,alpha:o},n="lab")=>{r===void 0&&(r=0);let a={mode:n,l:e,a:t?t*Math.cos(r/180*Math.PI):0,b:t?t*Math.sin(r/180*Math.PI):0};return o!==void 0&&(a.alpha=o),a},w=po;function uo(e,t){if(!t||t[0]!=="lch")return;let r={mode:"lch"},[,o,n,a,i]=t;if(o.type!==f.None){if(o.type===f.Hue)return;r.l=Math.min(Math.max(0,o.value),100)}if(n.type!==f.None&&(r.c=Math.max(0,n.type===f.Number?n.value:n.value*150/100)),a.type!==f.None){if(a.type===f.Percentage)return;r.h=a.value}return i.type!==f.None&&(r.alpha=Math.min(1,Math.max(0,i.type===f.Number?i.value:i.value/100))),r}var Nt=uo;var mo={mode:"lch",toMode:{lab:w,rgb:e=>ce(w(e))},fromMode:{rgb:e=>L(be(e)),lab:L},channels:["l","c","h","alpha"],ranges:{l:[0,100],c:[0,150],h:[0,360]},parse:[Nt],serialize:e=>`lch(${e.l!==void 0?e.l:"none"} ${e.c!==void 0?e.c:"none"} ${e.h!==void 0?e.h:"none"}${e.alpha<1?` / ${e.alpha}`:""})`,interpolate:{h:{use:l,fixup:k},c:l,l,alpha:{use:l,fixup:m}},difference:{h:qe},average:{h:X}},G=mo;var so={...G,mode:"lch65",parse:["--lch-d65"],serialize:"--lch-d65",toMode:{lab65:e=>w(e,"lab65"),rgb:e=>ve(w(e,"lab65"))},fromMode:{rgb:e=>L(Me(e),"lch65"),lab65:e=>L(e,"lch65")},ranges:{l:[0,100],c:[0,133.807],h:[0,360]}},Pt=so;var co={...x,mode:"lrgb",toMode:{rgb:T},fromMode:{rgb:y},parse:["srgb-linear"],serialize:"srgb-linear"},It=co;var bo=({l:e,a:t,b:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=Math.pow(e*.9999999984505198+.39633779217376786*t+.2158037580607588*r,3),a=Math.pow(e*1.0000000088817609-.10556134232365635*t-.06385417477170591*r,3),i=Math.pow(e*1.0000000546724108-.08948418209496575*t-1.2914855378640917*r,3),p={mode:"lrgb",r:4.076741661347994*n-3.307711590408193*a+.230969928729428*i,g:-1.2684380040921763*n+2.6097574006633715*a-.3413193963102197*i,b:-.004196086541837188*n-.7034186144594493*a+1.7076147009309444*i};return o!==void 0&&(p.alpha=o),p},Te=bo;var ho=({r:e,g:t,b:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=Math.cbrt(.41222147079999993*e+.5363325363*t+.0514459929*r),a=Math.cbrt(.2119034981999999*e+.6806995450999999*t+.1073969566*r),i=Math.cbrt(.08830246189999998*e+.2817188376*t+.6299787005000002*r),p={mode:"oklab",l:.2104542553*n+.793617785*a-.0040720468*i,a:1.9779984951*n-2.428592205*a+.4505937099*i,b:.0259040371*n+.7827717662*a-.808675766*i};return o!==void 0&&(p.alpha=o),p},ze=ho;var xo=e=>{let t=ze(y(e));return e.r===e.b&&e.b===e.g&&(t.a=t.b=0),t},Le=xo;var go=e=>T(Te(e)),we=go;function vo(e,t){if(!t||t[0]!=="oklab")return;let r={mode:"oklab"},[,o,n,a,i]=t;if(!(o.type===f.Hue||n.type===f.Hue||a.type===f.Hue))return o.type!==f.None&&(r.l=Math.min(Math.max(0,o.type===f.Number?o.value:o.value/100),1)),n.type!==f.None&&(r.a=n.type===f.Number?n.value:n.value*.4/100),a.type!==f.None&&(r.b=a.type===f.Number?a.value:a.value*.4/100),i.type!==f.None&&(r.alpha=Math.min(1,Math.max(0,i.type===f.Number?i.value:i.value/100))),r}var Ot=vo;var yo={...F,mode:"oklab",toMode:{lrgb:Te,rgb:we},fromMode:{lrgb:ze,rgb:Le},ranges:{l:[0,1],a:[-.4,.4],b:[-.4,.4]},parse:[Ot],serialize:e=>`oklab(${e.l!==void 0?e.l:"none"} ${e.a!==void 0?e.a:"none"} ${e.b!==void 0?e.b:"none"}${e.alpha<1?` / ${e.alpha}`:""})`},Ct=yo;function Mo(e,t){if(!t||t[0]!=="oklch")return;let r={mode:"oklch"},[,o,n,a,i]=t;if(o.type!==f.None){if(o.type===f.Hue)return;r.l=Math.min(Math.max(0,o.type===f.Number?o.value:o.value/100),1)}if(n.type!==f.None&&(r.c=Math.max(0,n.type===f.Number?n.value:n.value*.4/100)),a.type!==f.None){if(a.type===f.Percentage)return;r.h=a.value}return i.type!==f.None&&(r.alpha=Math.min(1,Math.max(0,i.type===f.Number?i.value:i.value/100))),r}var At=Mo;var To={...G,mode:"oklch",toMode:{oklab:e=>w(e,"oklab"),rgb:e=>we(w(e,"oklab"))},fromMode:{rgb:e=>L(Le(e),"oklch"),oklab:e=>L(e,"oklch")},parse:[At],serialize:e=>`oklch(${e.l!==void 0?e.l:"none"} ${e.c!==void 0?e.c:"none"} ${e.h!==void 0?e.h:"none"}${e.alpha<1?` / ${e.alpha}`:""})`,ranges:{l:[0,1],c:[0,.4],h:[0,360]}},qt=To;var zo=e=>{let{r:t,g:r,b:o,alpha:n}=y(e),a={mode:"xyz65",x:.486570948648216*t+.265667693169093*r+.1982172852343625*o,y:.2289745640697487*t+.6917385218365062*r+.079286914093745*o,z:0*t+.0451133818589026*r+1.043944368900976*o};return n!==void 0&&(a.alpha=n),a},Ze=zo;var Lo=({x:e,y:t,z:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n=T({r:e*2.4934969119414263-t*.9313836179191242-.402710784450717*r,g:e*-.8294889695615749+t*1.7626640603183465+.0236246858419436*r,b:e*.0358458302437845-t*.0761723892680418+.9568845240076871*r},"p3");return o!==void 0&&(n.alpha=o),n},Be=Lo;var wo={...x,mode:"p3",parse:["display-p3"],serialize:"display-p3",fromMode:{rgb:e=>Be(M(e)),xyz65:Be},toMode:{rgb:e=>z(Ze(e)),xyz65:Ze}},_t=wo;var je=e=>{let t=Math.abs(e);return t>=.001953125?Math.sign(e)*Math.pow(t,.5555555555555556):16*e},Ro=({x:e,y:t,z:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n={mode:"prophoto",r:je(e*1.3457868816471585-t*.2555720873797946-.0511018649755453*r),g:je(e*-.5446307051249019+t*1.5082477428451466+.0205274474364214*r),b:je(e*0+t*0+1.2119675456389452*r)};return o!==void 0&&(n.alpha=o),n},We=Ro;var Ke=(e=0)=>{let t=Math.abs(e);return t>=.03125?Math.sign(e)*Math.pow(t,1.8):e/16},Ho=e=>{let t=Ke(e.r),r=Ke(e.g),o=Ke(e.b),n={mode:"xyz50",x:.7977666449006423*t+.1351812974005331*r+.0313477341283922*o,y:.2880748288194013*t+.7118352342418731*r+899369387256e-16*o,z:0*t+0*r+.8251046025104602*o};return e.alpha!==void 0&&(n.alpha=e.alpha),n},Qe=Ho;var ko={...x,mode:"prophoto",parse:["prophoto-rgb"],serialize:"prophoto-rgb",fromMode:{xyz50:We,rgb:e=>We(J(e))},toMode:{xyz50:Qe,rgb:e=>E(Qe(e))}},Dt=ko;var Et=1.09929682680944,Xo=.018053968510807,Ue=e=>{let t=Math.abs(e);return t>Xo?(Math.sign(e)||1)*(Et*Math.pow(t,.45)-(Et-1)):4.5*e},$o=({x:e,y:t,z:r,alpha:o})=>{e===void 0&&(e=0),t===void 0&&(t=0),r===void 0&&(r=0);let n={mode:"rec2020",r:Ue(e*1.7166511879712683-t*.3556707837763925-.2533662813736599*r),g:Ue(e*-.6666843518324893+t*1.6164812366349395+.0157685458139111*r),b:Ue(e*.0176398574453108-t*.0427706132578085+.9421031212354739*r)};return o!==void 0&&(n.alpha=o),n},Ve=$o;var Jt=1.09929682680944,So=.018053968510807,et=(e=0)=>{let t=Math.abs(e);return t{let t=et(e.r),r=et(e.g),o=et(e.b),n={mode:"xyz65",x:.6369580483012911*t+.1446169035862083*r+.1688809751641721*o,y:.262700212011267*t+.6779980715188708*r+.059301716469862*o,z:0*t+.0280726930490874*r+1.0609850577107909*o};return e.alpha!==void 0&&(n.alpha=e.alpha),n},tt=No;var Po={...x,mode:"rec2020",fromMode:{xyz65:Ve,rgb:e=>Ve(M(e))},toMode:{xyz65:tt,rgb:e=>z(tt(e))},parse:["rec2020"],serialize:"rec2020"},Yt=Po;var Io={mode:"xyz50",parse:["xyz-d50"],serialize:"xyz-d50",toMode:{rgb:E,lab:Y},fromMode:{rgb:J,lab:D},channels:["x","y","z","alpha"],ranges:{x:[0,.964],y:[0,.999],z:[0,.825]},interpolate:{x:l,y:l,z:l,alpha:{use:l,fixup:m}}},Ft=Io;var Oo=e=>{let{x:t,y:r,z:o,alpha:n}=e;t===void 0&&(t=0),r===void 0&&(r=0),o===void 0&&(o=0);let a={mode:"xyz50",x:1.0479298208405488*t+.0229467933410191*r-.0501922295431356*o,y:.0296278156881593*t+.990434484573249*r-.0170738250293851*o,z:-.0092430581525912*t+.0150551448965779*r+.7518742899580008*o};return n!==void 0&&(a.alpha=n),a},Gt=Oo;var Co=e=>{let{x:t,y:r,z:o,alpha:n}=e;t===void 0&&(t=0),r===void 0&&(r=0),o===void 0&&(o=0);let a={mode:"xyz65",x:.9554734527042182*t-.0230985368742614*r+.0632593086610217*o,y:-.0283697069632081*t+1.0099954580058226*r+.021041398966943*o,z:.0123140016883199*t-.0205076964334779*r+1.3303659366080753*o};return n!==void 0&&(a.alpha=n),a},Zt=Co;var Ao={mode:"xyz65",toMode:{rgb:z,xyz50:Gt},fromMode:{rgb:M,xyz50:Zt},ranges:{x:[0,.95],y:[0,1],z:[0,1.088]},channels:["x","y","z","alpha"],parse:["xyz","xyz-d65"],serialize:"xyz-d65",interpolate:{x:l,y:l,z:l,alpha:{use:l,fixup:m}}},Bt=Ao;var Yi=u(Mt),Fi=u(Rt),Gi=u(Ht),Zi=u(Xt),Bi=u(F),ji=u(St),Wi=u(G),Ki=u(Pt),Qi=u(It),Ui=u(Ct),Vi=u(qt),ed=u(_t),td=u(Dt),rd=u(Yt),od=u(x),nd=u(Ft),ad=u(Bt);var qo=(e,t)=>Math.round(e*(t=Math.pow(10,t)))/t,_o=(e=4)=>t=>typeof t=="number"?qo(t,e):t,jt=_o;var md=jt(2),Do=e=>Math.max(0,Math.min(1,e||0)),rt=e=>Math.round(Do(e)*255),Eo=h("rgb"),sd=h("hsl"),Wt=e=>{if(e===void 0)return;let t=rt(e.r),r=rt(e.g),o=rt(e.b);return"#"+(1<<24|t<<16|r<<8|o).toString(16).slice(1)};var Kt=e=>{let t=ie(e);if(!t)return;let r=_(t.mode);if(!r.serialize||typeof r.serialize=="string"){let o=`color(${r.serialize||`--${t.mode}`} `;return r.channels.forEach((n,a)=>{n!=="alpha"&&(o+=(a?" ":"")+(t[n]!==void 0?t[n]:"none"))}),t.alpha!==void 0&&t.alpha<1&&(o+=` / ${t.alpha}`),o+")"}if(typeof r.serialize=="function")return r.serialize(t)},Qt=e=>Wt(Eo(e));var Jo=(e,t=_e(),r=o=>o)=>{let o=e.map((n,a)=>({color:r(n),i:a}));return(n,a=1,i=1/0)=>(isFinite(a)&&(a=Math.max(1,Math.min(a,o.length-1))),o.forEach(p=>{p.d=t(n,p.color)}),o.sort((p,b)=>p.d-b.d).slice(0,a).filter(p=>p.de[p.i]))},Ut=Jo;function ot(e){let t=h("lrgb")(e);return .2126*t.r+.7152*t.g+.0722*t.b}function Vt(e,t){let r=ot(e),o=ot(t);return(Math.max(r,o)+.05)/(Math.min(r,o)+.05)}var Ed=h("oklab");export{h as converter,wt as differenceCiede2000,Kt as formatCss,Qt as formatHex,Ut as nearest,Ed as oklab,de as parse,fe as parseHex,Vt as wcagContrast}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..080f118 --- /dev/null +++ b/index.html @@ -0,0 +1,136 @@ + + + + + + + Automatic colour palette maker + + + + + + + + + +
+

+ Automatic colour palette maker +

+
+
+
+

+ Explanation +

+ +

Pick colours for each of the styles.

+

Related colours will be automatically calculated: a slightly darker border, a contrasting text colour, and hover/focus/active highlights. +

+ +
+

+ Pick colours +

+
+
+
+ +
+

+ Preview +

+ + + +
+

A heading

+ +

A long line of text, which is pretty good as far as things go.

+ +
+ +

Text after a horizontal rule.

+ +
+ +

Text after a faint horizontal rule.

+ + + + + + + + +
Heading
ab
+ +

Help text

+ + + + + + + + + +

Text with monospace code

+ +
+pre-formatted
+text
+            
+ +

Text to press a key.

+
+ +
+ +

+ Text with + a link + and text styled + success or + info or + warning or + danger or + muted. +

+ +

+ + + + + + +

+
+ +
+
+
+ + + + diff --git a/scheme.css b/scheme.css new file mode 100644 index 0000000..5fdee89 --- /dev/null +++ b/scheme.css @@ -0,0 +1,229 @@ +:root { + --highlight: white; + --off: black; + + --styled-text-mix: 20%; + + --default-style-mix: 10%; + --faint-bg-mix: 10%; + --faint-line-mix: 20%; + + font-size: 18px; + font-family: sans-serif; +} + +:root.dark-background { + --default-style-mix: 40%; + --faint-bg-mix: 20%; + --faint-line-mix: 40%; + --highlight: black; + --off: white; +} + +label { + display: block; +} + +* { + --styled-background: color-mix(in oklab, var(--background), var(--text) var(--default-style-mix)); + --styled-text: var(--text); + + --medium-line-color: color-mix(in oklab, var(--background), var(--text) 50%); + --faint-line-color: color-mix(in oklab, var(--background), var(--text) var(--faint-line-mix)); + --faint-background: color-mix(in oklab, var(--background), var(--text) var(--faint-bg-mix)); +} + +#color-picker { + display: grid; + grid-template-columns: repeat(auto-fill, 10em); + grid-gap: 1em; + margin: 1em 0; + text-align: center; + text-transform: capitalize; + + & input { + width: 100%; + } +} + +#preview { + background-color: var(--background); + color: var(--text); + padding: 1em; + + + & a { + --highlight-amount: 0%; + --off-amount: 0%; + color: + color-mix(in oklab, + color-mix(in oklab, + var(--link), + var(--highlight) + var(--highlight-amount) + ), + var(--off) + var(--off-amount) + ); + + &:hover { + --highlight-amount: 20%; + } + &:focus { + --highlight-amount: 40%; + } + &:active { + --off-amount: 20%; + } + &:visited { + --off-amount: 40%; + } + } + + & .success { + color: color-mix(in oklab, var(--success), var(--text) var(--styled-text-mix)); + --styled-background: var(--success); + --styled-text: var(--success-text); + } + + & .primary { + color: color-mix(in oklab, var(--primary), var(--text) var(--styled-text-mix)); + --styled-background: var(--primary); + --styled-text: var(--primary-text); + } + + & .info { + color: color-mix(in oklab, var(--info), var(--text) var(--styled-text-mix)); + --styled-background: var(--info); + --styled-text: var(--info-text); + } + + & .warning { + color: color-mix(in oklab, var(--warning), var(--text) var(--styled-text-mix)); + --styled-background: var(--warning); + --styled-text: var(--warning-text); + } + + & .danger { + color: color-mix(in oklab, var(--danger), var(--text) var(--styled-text-mix)); + --styled-background: var(--danger); + --styled-text: var(--danger-text); + } + + & .muted { + color: color-mix(in oklab, var(--muted), var(--text) var(--styled-text-mix)); + --styled-background: var(--muted); + --styled-text: var(--muted-text); + } + + button:is(*, .success, .info, .warning, .danger, .muted, .primary) { + --highlight-amount: 0%; + --off-amount: 0%; + --mixed-styled-background: + color-mix(in oklab, + color-mix(in oklab, + var(--styled-background), + var(--highlight) + var(--highlight-amount) + ), + var(--off) + var(--off-amount) + ); + --button-border: color-mix(in oklab, var(--mixed-styled-background), var(--off) 30%); + background-color: var(--mixed-styled-background); + border: medium solid var(--button-border); + color: var(--styled-text); + font-size: 1em; + + &:hover { + --highlight-amount: 20%; + } + &:focus { + --highlight-amount: 40%; + } + &:active { + --off-amount: 20%; + } + + &.info { + --styled-background: var(--info); + color: var(--info-text); + } + + &.warning { + --styled-background: var(--warning); + color: var(--warning-text); + } + + &.danger { + --styled-background: var(--danger); + color: var(--danger-text); + } + + &.primary { + --styled-background: var(--primary); + color: var(--primary-text); + } + padding: 0.25em 0.5em; + } + + & .alert { + padding: 1em; + margin: 1em 0; + --alert-background: color-mix(in oklab, var(--styled-background), var(--text) 10%); + --alert-border: color-mix(in oklab, var(--alert-background), var(--off) 10%); + color: var(--styled-text); + + background-color: var(--alert-background); + + border: medium solid var(--alert-border); + } + + & nav { + background: var(--main); + color: var(--main-text); + padding: 0.5em; + } + + & hr { + border: thin solid var(--medium-line-color); + + &.faint { + border: thin solid var(--faint-line-color); + } + } + + & input { + border: thin solid var(--faint-line-color); + border-bottom-color: var(--text); + border-bottom-width: thin; + background-color: var(--background); + color: var(--text); + font-size: 1em; + --styled-background: var(--background); + background-color: color-mix(in oklab, var(--background), var(--styled-background) var(--faint-line-mix)); + + &.answered { + --styled-background: var(--info); + } + + &.has-errors { + --styled-background: var(--danger); + } + + &.has-warnings { + --styled-background: var(--warning); + } + } + + & pre,code, kbd { + background: var(--faint-background); + padding: 0.5em; + } + + & kbd { + border: thin solid var(--medium-line-color); + padding: 0.2em; + margin: 0 0.2em; + } +} \ No newline at end of file diff --git a/scheme.js b/scheme.js new file mode 100644 index 0000000..748eedd --- /dev/null +++ b/scheme.js @@ -0,0 +1,82 @@ +import {parseRGB, convertRgbToOklch, convertRgbToLab65, mix} from './convert.js'; + +function el(name, attr, content) { + const element = document.createElement(name); + if(attr) { + for(let [k,v] of Object.entries(attr)) { + element.setAttribute(k, v); + } + } + if(content) { + element.innerHTML = content; + } + return element; +} + +const colour_groups = [ + {name: 'background', value: '#ffffff'}, + {name: 'text', value: '#000000'}, + {name: 'main', value: '#a2d1f0'}, + {name: 'primary', value: '#76f2ff'}, + {name: 'link', value: '#0000ee'}, + {name: 'success', value: '#008000'}, + {name: 'info', value: '#0c6dcf'}, + {name: 'warning', value: '#996700'}, + {name: 'danger', value: '#df0000'}, + {name: 'muted', value: '#636363'} +]; + +const color_picker_form = document.getElementById('color-picker'); + +/* + Delta Phi Star perceptual lightness contrast by Andrew Somers: + https://github.com/Myndex/deltaphistar +*/ +const PHI = (1 + Math.sqrt(5))/2; + +function dpsContrast(a, b) { + const dps = Math.abs(Math.pow(convertRgbToLab65(a).l, PHI) - Math.pow(convertRgbToLab65(b).l, PHI)); + const contrast = Math.pow(dps, 1/PHI) * Math.SQRT2 - 40; + return contrast < 7.5 ? 0 : contrast; +} + +function is_dark(col) { + const black_contrast = dpsContrast(col, {r:0, g:0, b:0}); + const white_contrast = dpsContrast(col, {r:1, g:1, b:1}); + return black_contrast < white_contrast; +} + +function text_for(col) { + return is_dark(col) ? 'white' : 'black'; +} + +colour_groups.forEach(({name, value}) => { + const label = el('label',{}, name); + const picker = el('input', {type: 'color', value}); + label.append(picker); + color_picker_form.appendChild(label); + + function update() { + const col = picker.value; + const rgb = parseRGB(col); + const oklch = convertRgbToOklch(rgb); + console.log(name, col, oklch); + const root = document.documentElement; + root.style.setProperty(`--${name}`, col); + root.style.setProperty(`--${name}-text`, text_for(rgb)); + if(name == 'background') { + is_dark(rgb) ? root.classList.add('dark-background') : root.classList.remove('dark-background'); + } + } + picker.addEventListener('input', update); + update(); +}); + +['','success','info','warning','danger','muted'].forEach(name => { + const alertDiv = el('div',{class:`alert ${name}`}); + alertDiv.appendChild(el('h2',{},name)); + alertDiv.appendChild(document.getElementById('plain-text').cloneNode(true)); + document.getElementById('alerts').appendChild(alertDiv); +}); + +window.parseRGB = parseRGB; \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..44f6cca --- /dev/null +++ b/script.js @@ -0,0 +1,134 @@ +import {calcAPCA} from './apca-w3.js'; +import { wcagContrast, formatHex} from './culori.mjs'; +import * as culori from './culori.mjs'; +import {name_colour, nearestNamedColors} from './color-picker.js'; +import {find_contrast} from './auto_contrast.js'; + +import * as plain_converters from './convert.js'; + +window.plain_converters = plain_converters; + +window.culori = culori; + +const color_a_input = document.getElementById('color-a'); +const color_b_input = document.getElementById('color-b'); + +export class PaletteInfoElement extends HTMLElement { + + static observedAttributes = ['text', 'bg']; + + constructor() { + super(); + let template = document.getElementById("palette-info-template"); + let templateContent = template.content; + + const sheet = new CSSStyleSheet(); + sheet.replaceSync(` + :root { + --col-a: black; + --col-b: white; + } + + #container { + display: grid; + grid-gap: 0.5em; + grid-template-columns: 1fr auto; + } + + #preview .block { + padding: 0.5em; + color: var(--col-a); + background: var(--col-b); + } + #preview p { + padding: 1em; + } + #preview #inline-white-bg { + color: black; + background: white; + } + #preview #inline-black-bg { + color: white; + background: black; + } + #preview .inline { + color: var(--col-a); + } + p { + margin: 0 0 0.5em 0; + } + `); + + const shadowRoot = this.attachShadow({ mode: "open" }); + shadowRoot.appendChild(templateContent.cloneNode(true)); + shadowRoot.adoptedStyleSheets = [sheet]; + + this.set_colours(); + } + + attributeChangedCallback(name, oldValue, newValue) { + this.set_colours(); + } + + set_colours() { + const col_a = this.getAttribute('text'); + const col_b = this.getAttribute('bg'); + const hex_a = formatHex(col_a); + const hex_b = formatHex(col_b); + const apca_contrast = Math.abs(calcAPCA(hex_a, hex_b)); + this.shadowRoot.getElementById('apca-contrast').textContent = apca_contrast.toFixed(0); + const wcag_contrast = wcagContrast(col_a, col_b); + this.shadowRoot.getElementById('wcag-contrast').textContent = wcag_contrast.toFixed(2); + const preview = this.shadowRoot.getElementById('preview'); + preview.style.setProperty('--col-a',hex_a); + preview.style.setProperty('--col-b',hex_b); + + } +} + +customElements.define('palette-info', PaletteInfoElement); + +class ColorPaletteApp { + constructor() { + Array.from(document.querySelectorAll('color-picker')).forEach(i => i.addEventListener('input', () => this.update())); + this.current_palette = document.querySelector('palette-info'); + + document.getElementById('fiddle-text').addEventListener('click', () => this.fiddle_text()); + document.getElementById('fiddle-bg').addEventListener('click', () => this.fiddle_bg()); + } + + update() { + const col_a = color_a_input.value; + const col_b = color_b_input.value; + const hex_a = formatHex(col_a); + const hex_b = formatHex(col_b); + this.current_palette.setAttribute('text',hex_a); + this.current_palette.setAttribute('bg',hex_b); + } + + get target_contrast() { + return document.getElementById('target-contrast').valueAsNumber; + } + + get num_close_colours() { + return document.getElementById('num-close-colours').valueAsNumber; + } + + fiddle_text() { + const col_a = culori.oklab(color_a_input.value); + const col_b = culori.oklab(color_b_input.value); + color_a_input.value = formatHex(find_contrast(col_b, col_a, this.target_contrast, this.num_close_colours)); + } + + fiddle_bg() { + const col_a = culori.oklab(color_a_input.value); + const col_b = culori.oklab(color_b_input.value); + color_b_input.value = formatHex(find_contrast(col_a, col_b, this.target_contrast, this.num_close_colours)); + } + +} + +const app = new ColorPaletteApp(); +window.culori = culori; + +app.update(); \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..8508a59 --- /dev/null +++ b/style.css @@ -0,0 +1,42 @@ +:root { + --col-a: black; + --col-b: white; +} + +palette-info { + display: block; + margin: 0.2em 0; +} + +ul { + list-style: none; + padding: 0; +} + +#optimise input { + max-width: 5em; + text-align: center; +} + +@media (min-width: 60em) { + #color-pickers { + display: grid; + grid-auto-flow: column; + } +} + +footer { + margin-top: 5em; +} + +section ~ section { + margin-top: 3em; +} + +th, td { + padding: 0.5em; +} + +td { + text-align: right; +} \ No newline at end of file