From 415ab2bb946e9efac2571245745ef56ec9da5e17 Mon Sep 17 00:00:00 2001 From: DavidHarvey Date: Tue, 6 Jan 2026 17:36:29 -0500 Subject: [PATCH 1/2] Port newer color readability functions from TinyColor --- packages/core/src/utils/ColorPicker.ts | 131 ++++++++++++++++--------- 1 file changed, 87 insertions(+), 44 deletions(-) diff --git a/packages/core/src/utils/ColorPicker.ts b/packages/core/src/utils/ColorPicker.ts index 8ec90f938..dd651e99b 100644 --- a/packages/core/src/utils/ColorPicker.ts +++ b/packages/core/src/utils/ColorPicker.ts @@ -1343,6 +1343,18 @@ export default function ($, undefined?: any) { var rgb = this.toRgb(); return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000; }, + getLuminance: function () { + // http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + var rgb = this.toRgb(); + var RsRGB, GsRGB, BsRGB, R, G, B; + RsRGB = rgb.r / 255; + GsRGB = rgb.g / 255; + BsRGB = rgb.b / 255; + if (RsRGB <= 0.03928) R = RsRGB / 12.92;else R = Math.pow((RsRGB + 0.055) / 1.055, 2.4); + if (GsRGB <= 0.03928) G = GsRGB / 12.92;else G = Math.pow((GsRGB + 0.055) / 1.055, 2.4); + if (BsRGB <= 0.03928) B = BsRGB / 12.92;else B = Math.pow((BsRGB + 0.055) / 1.055, 2.4); + return 0.2126 * R + 0.7152 * G + 0.0722 * B; + }, setAlpha: function (value) { this._a = boundAlpha(value); this._roundA = mathRound(100 * this._a) / 100; @@ -2013,71 +2025,80 @@ export default function ($, undefined?: any) { // Readability Functions // --------------------- - // + // false - tinycolor.isReadable = function (color1, color2) { + // tinycolor.isReadable("#000", "#111",{level:"AA",size:"large"}) => false + tinycolor.isReadable = function (color1, color2, wcag2) { var readability = tinycolor.readability(color1, color2); - return readability.brightness > 125 && readability.color > 500; + var wcag2Parms, out; + out = false; + wcag2Parms = validateWCAG2Parms(wcag2); + switch (wcag2Parms.level + wcag2Parms.size) { + case "AAsmall": + case "AAAlarge": + out = readability >= 4.5; + break; + case "AAlarge": + out = readability >= 3; + break; + case "AAAsmall": + out = readability >= 7; + break; + } + return out; }; // `mostReadable` // Given a base color and a list of possible foreground or background // colors for that base, returns the most readable color. + // Optionally returns Black or White if the most readable color is unreadable. // *Example* - // tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000" - tinycolor.mostReadable = function (baseColor, colorList) { + // tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255" + // tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(); // "#ffffff" + // tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(); // "#faf3f3" + // tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(); // "#ffffff" + tinycolor.mostReadable = function (baseColor, colorList, args) { var bestColor = null; var bestScore = 0; - var bestIsReadable = false; + var readability; + var includeFallbackColors, level, size; + args = args || {}; + includeFallbackColors = args.includeFallbackColors; + level = args.level; + size = args.size; for (var i = 0; i < colorList.length; i++) { - // We normalize both around the "acceptable" breaking point, - // but rank brightness constrast higher than hue. - - var readability = tinycolor.readability(baseColor, colorList[i]); - var readable = readability.brightness > 125 && readability.color > 500; - var score = 3 * (readability.brightness / 125) + readability.color / 500; - - if ( - (readable && !bestIsReadable) || - (readable && bestIsReadable && score > bestScore) || - (!readable && !bestIsReadable && score > bestScore) - ) { - bestIsReadable = readable; - bestScore = score; + readability = tinycolor.readability(baseColor, colorList[i]); + if (readability > bestScore) { + bestScore = readability; bestColor = tinycolor(colorList[i]); } } - return bestColor; + if (tinycolor.isReadable(baseColor, bestColor, { + level: level, + size: size + }) || !includeFallbackColors) { + return bestColor; + } else { + args.includeFallbackColors = false; + return tinycolor.mostReadable(baseColor, ["#fff", "#000"], args); + } }; // Big List of Colors @@ -2433,6 +2454,28 @@ export default function ($, undefined?: any) { return false; } + function validateWCAG2Parms(parms) { + // return valid WCAG2 parms for isReadable. + // If input parms are invalid, return {'level':'AA', 'size':'small'} + var level, size; + parms = parms || { + level: 'AA', + size: 'small' + }; + level = (parms.level || 'AA').toUpperCase(); + size = (parms.size || 'small').toLowerCase(); + if (level !== 'AA' && level !== 'AAA') { + level = 'AA'; + } + if (size !== 'small' && size !== 'large') { + size = 'small'; + } + return { + level: level, + size: size + }; + } + window.tinycolor = tinycolor; //})(); From 1de2b69dac9f35a5392667cd3063544247953c69 Mon Sep 17 00:00:00 2001 From: DavidHarvey Date: Tue, 6 Jan 2026 17:59:05 -0500 Subject: [PATCH 2/2] Formatting --- packages/core/src/utils/ColorPicker.ts | 38 ++++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/core/src/utils/ColorPicker.ts b/packages/core/src/utils/ColorPicker.ts index dd651e99b..d6a8aa49e 100644 --- a/packages/core/src/utils/ColorPicker.ts +++ b/packages/core/src/utils/ColorPicker.ts @@ -1350,9 +1350,12 @@ export default function ($, undefined?: any) { RsRGB = rgb.r / 255; GsRGB = rgb.g / 255; BsRGB = rgb.b / 255; - if (RsRGB <= 0.03928) R = RsRGB / 12.92;else R = Math.pow((RsRGB + 0.055) / 1.055, 2.4); - if (GsRGB <= 0.03928) G = GsRGB / 12.92;else G = Math.pow((GsRGB + 0.055) / 1.055, 2.4); - if (BsRGB <= 0.03928) B = BsRGB / 12.92;else B = Math.pow((BsRGB + 0.055) / 1.055, 2.4); + if (RsRGB <= 0.03928) R = RsRGB / 12.92; + else R = Math.pow((RsRGB + 0.055) / 1.055, 2.4); + if (GsRGB <= 0.03928) G = GsRGB / 12.92; + else G = Math.pow((GsRGB + 0.055) / 1.055, 2.4); + if (BsRGB <= 0.03928) B = BsRGB / 12.92; + else B = Math.pow((BsRGB + 0.055) / 1.055, 2.4); return 0.2126 * R + 0.7152 * G + 0.0722 * B; }, setAlpha: function (value) { @@ -2032,7 +2035,9 @@ export default function ($, undefined?: any) { tinycolor.readability = function (color1, color2) { var c1 = tinycolor(color1); var c2 = tinycolor(color2); - return (Math.max(c1.getLuminance(), c2.getLuminance()) + 0.05) / (Math.min(c1.getLuminance(), c2.getLuminance()) + 0.05); + return ( + (Math.max(c1.getLuminance(), c2.getLuminance()) + 0.05) / (Math.min(c1.getLuminance(), c2.getLuminance()) + 0.05) + ); }; // `isReadable` @@ -2051,14 +2056,14 @@ export default function ($, undefined?: any) { out = false; wcag2Parms = validateWCAG2Parms(wcag2); switch (wcag2Parms.level + wcag2Parms.size) { - case "AAsmall": - case "AAAlarge": + case 'AAsmall': + case 'AAAlarge': out = readability >= 4.5; break; - case "AAlarge": + case 'AAlarge': out = readability >= 3; break; - case "AAAsmall": + case 'AAAsmall': out = readability >= 7; break; } @@ -2090,14 +2095,17 @@ export default function ($, undefined?: any) { bestColor = tinycolor(colorList[i]); } } - if (tinycolor.isReadable(baseColor, bestColor, { - level: level, - size: size - }) || !includeFallbackColors) { + if ( + tinycolor.isReadable(baseColor, bestColor, { + level: level, + size: size, + }) || + !includeFallbackColors + ) { return bestColor; } else { args.includeFallbackColors = false; - return tinycolor.mostReadable(baseColor, ["#fff", "#000"], args); + return tinycolor.mostReadable(baseColor, ['#fff', '#000'], args); } }; @@ -2460,7 +2468,7 @@ export default function ($, undefined?: any) { var level, size; parms = parms || { level: 'AA', - size: 'small' + size: 'small', }; level = (parms.level || 'AA').toUpperCase(); size = (parms.size || 'small').toLowerCase(); @@ -2472,7 +2480,7 @@ export default function ($, undefined?: any) { } return { level: level, - size: size + size: size, }; }