first commit
This commit is contained in:
commit
89c3ddb84b
14 changed files with 1886 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.make.*
|
54
LICENSE
Normal file
54
LICENSE
Normal 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
0
README.md
Normal file
763
apca-w3.js
Normal file
763
apca-w3.js
Normal 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
34
auto_contrast.js
Normal 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
158
color-picker.js
Normal 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
30
colour_names.js
Normal 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
219
convert.js
Normal 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
4
culori.mjs
Normal file
File diff suppressed because one or more lines are too long
136
index.html
Normal file
136
index.html
Normal 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
229
scheme.css
Normal 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
82
scheme.js
Normal 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
134
script.js
Normal 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
42
style.css
Normal 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;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue