first commit

This commit is contained in:
Christian Lawson-Perfect 2025-02-09 20:19:13 +00:00
commit 89c3ddb84b
14 changed files with 1886 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.make.*

54
LICENSE Normal file
View file

@ -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
******************************************************************************

0
README.md Normal file
View file

763
apca-w3.js Normal file
View file

@ -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[0]||
Math.max(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)+'<sub>'+polCat+'</sub>';
} 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 ////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

34
auto_contrast.js Normal file
View file

@ -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<cb ? -1 : 1});
tcol_b = best.ncol;
temp *= 0.9;
}
const [{name}] = nearestNamedColors(tcol_b,1);
return tcol_b;
}

158
color-picker.js Normal file
View file

@ -0,0 +1,158 @@
import colour_names from './colour_names.js';
import { formatHex, parseHex, converter, differenceCiede2000, formatCss } from './culori.mjs';
import * as culori from './culori.mjs';
const COLOUR_WEIGHT = 1.2;
const convertToLab = converter('lab');
const convertToOklab = converter('oklab');
const color_difference = differenceCiede2000();
const weighted_color_difference = (a,b) => {
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
);

30
colour_names.js Normal file
View file

@ -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;

219
convert.js Normal file
View file

@ -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};

4
culori.mjs Normal file

File diff suppressed because one or more lines are too long

136
index.html Normal file
View file

@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Automatic colour palette maker</title>
<meta
name="description"
content="A tool to help me, with limited colour vision, make accessible colour palettes."
/>
<meta name="robots" content="index,follow" />
<link rel="stylesheet" href="/scheme.css" />
<script src="/scheme.js" type="module"></script>
<body>
<header>
<h1>
Automatic colour palette maker
</h1>
</header>
<main>
<section>
<h2>
Explanation
</h2>
<p>Pick colours for each of the styles.</p>
<p>Related colours will be automatically calculated: a slightly darker border, a contrasting text colour, and hover/focus/active highlights.
</section>
<section>
<h2>
Pick colours
</h2>
<form id="color-picker">
</form>
</section>
<section id="preview">
<h2>
Preview
</h2>
<nav>
<p>Text in a nav <a href="#somewhere">link</a></p>
<p>
<button>default</button>
<button class="success">success</button>
<button class="info">info</button>
<button class="warning">warning</button>
<button class="danger">danger</button>
<button class="primary">primary</button>
</p>
</nav>
<div>
<h2>A heading</h2>
<p>A long line of text, which is pretty good as far as things go.</p>
<hr>
<p>Text after a horizontal rule.</p>
<hr class="faint">
<p>Text after a faint horizontal rule.</p>
<table>
<thead>
<tr><th>H</th><th>eading</th></tr>
</thead>
<tbody>
<tr><td>a</td><td>b</td></tr>
</tbody>
</table>
<p class="help-block muted">Help text</p>
<input type="text" value="text"></input>
<input class="answered" value="answered"></input>
<input class="has-errors" value="has errors"></input>
<input class="has-warnings" value="has warnings"></input>
<p>Text with <code>monospace code</code></p>
<pre>
pre-formatted
text
</pre>
<p>Text to press a <kbd>k</kbd>ey.</p>
</div>
<div id="plain-text">
<p>
Text with
<a href="#somewhere">a link</a>
and text styled
<span class="success">success</span> or
<span class="info">info</span> or
<span class="warning">warning</span> or
<span class="danger">danger</span> or
<span class="muted">muted</span>.
</p>
<p>
<button>default</button>
<button class="success">success</button>
<button class="info">info</button>
<button class="warning">warning</button>
<button class="danger">danger</button>
<button class="primary">primary</button>
</p>
</div>
<div id="alerts"></div>
</section>
</main>
<footer>
<p>
Made by <a href="https://somethingorotherwhatever.com">clp</a>.
</p>
</footer>
</body>
</html>

229
scheme.css Normal file
View file

@ -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;
}
}

82
scheme.js Normal file
View file

@ -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;

134
script.js Normal file
View file

@ -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();

42
style.css Normal file
View file

@ -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;
}