From 4560a34cd2651bf2c0bc18cf0f6f447522f3ade7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rebelo?= Date: Fri, 11 Aug 2023 18:54:10 +0100 Subject: [PATCH] feat: Colour Accessibility restriction #56 #45 #25 --- evo-poster.config.js | 9 +- src/client/controllers/ColorGenerator.js | 41 +++++ src/client/controllers/Population.js | 16 +- src/client/controllers/Poster.js | 17 +- src/public/app.js | 200 +++++++++++++++-------- 5 files changed, 208 insertions(+), 75 deletions(-) diff --git a/evo-poster.config.js b/evo-poster.config.js index c7d1ac0..56c4e53 100644 --- a/evo-poster.config.js +++ b/evo-poster.config.js @@ -50,6 +50,13 @@ const TYPEFACES = { } } +const COLOR = { + MIN_CONTRAST: 2.5, + MAX_COLOR_SCHEME_ATTEMPT: 200, +} + + export default { - typography: TYPEFACES !== undefined ? TYPEFACES : {} + typography: TYPEFACES !== undefined ? TYPEFACES : {}, + color: COLOR !== undefined ? COLOR : {} } \ No newline at end of file diff --git a/src/client/controllers/ColorGenerator.js b/src/client/controllers/ColorGenerator.js index 7c8d780..ba76afe 100644 --- a/src/client/controllers/ColorGenerator.js +++ b/src/client/controllers/ColorGenerator.js @@ -1,12 +1,24 @@ import {harmony, analogue, complement, triad } from 'simpler-color'; +import * as config from '../../../evo-poster.config.js'; + +const MIN_CONTRAST = config["default"].color !== null ? config["default"].color.MIN_CONTRAST : 10; export const randomScheme = () => { const baseColour = randomColour(); return complementAndAnalogueScheme(baseColour); } +export const contrastChecker = (baseColor, colorA, colorB) => { + const baseColorLuminance = luminance(baseColor); + const colorALuminance = luminance(colorA); + const colorBLuminance = luminance(colorB); + const min = Math.min(contrastRatio(baseColorLuminance, colorALuminance), contrastRatio(baseColorLuminance, colorBLuminance)); + return min > MIN_CONTRAST; +} + export const complementAndAnalogueScheme = (baseColour) => { const colorA = complement(baseColour, 1); + const colorB = analogue(colorA, Math.round(-2+(Math.random()*4))); return { baseColour: baseColour, @@ -55,4 +67,33 @@ export const randomColour = () => { } + +const hexToRGB = (hex) => { + let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +const luminance = (c) => { + c = hexToRGB(c); + let [lumR, lumG, lumB] = Object.keys(c).map(key => { + let proportion = c[key] / 255; + return proportion <= 0.03928 + ? proportion / 12.92 + : Math.pow((proportion + 0.055) / 1.055, 2.4); + }); + return 0.2126 * lumR + 0.7152 * lumG + 0.0722 * lumB; +} + +const contrastRatio = (luminance1, luminance2) => { + let lighterLum = Math.max(luminance1, luminance2); + let darkerLum = Math.min(luminance1, luminance2); + + return (lighterLum + 0.05) / (darkerLum + 0.05); +} + + export default randomScheme; \ No newline at end of file diff --git a/src/client/controllers/Population.js b/src/client/controllers/Population.js index af7313a..47cda74 100644 --- a/src/client/controllers/Population.js +++ b/src/client/controllers/Population.js @@ -1,10 +1,14 @@ import {Params} from "../Params.js"; import Poster, {Grid} from "./Poster.js"; -import {randomScheme} from "./ColorGenerator.js"; +import {contrastChecker, randomScheme} from "./ColorGenerator.js"; import {shuffleArr, sumArr, sus, swap} from "../utils.js"; +import * as config from './../../../evo-poster.config.js'; + + const SIZE_MUTATION_ADJUST = 5; const TOURNAMENT_SIZE = 10; +const MAX_COLOR_SCHEME_ATTEMPT = config["default"]["COLOR"] !== undefined ? config["default"]["COLOR"]["MAX_COLOR_SCHEME_ATTEMPT"] : 200; export class Population { #typefaces; @@ -196,7 +200,15 @@ export class Population { // colours scheme if (Math.random() < prob) { - const colorScheme = randomScheme(); + let colorContrast = false; + let colorScheme; + let colorAttempt = 0; + while (!colorContrast || colorAttempt > MAX_COLOR_SCHEME_ATTEMPT) { + colorScheme = randomScheme(); + colorContrast = contrastChecker(colorScheme["baseColour"], colorScheme["colorA"], colorScheme["colorB"]); + colorAttempt++; + } + // mutate background colours if (!this.params["background"]["lock"][1]) { ind.genotype["background"]["colors"][0] = colorScheme.colorA; diff --git a/src/client/controllers/Poster.js b/src/client/controllers/Poster.js index 9bf357e..8df06e0 100644 --- a/src/client/controllers/Poster.js +++ b/src/client/controllers/Poster.js @@ -2,10 +2,13 @@ import {Params} from "../Params.js"; import backgroundStyles from "./BackgroundStyles.js"; import * as evaluator from "../../@evoposter/evaluator/src/index.mjs"; -import {randomScheme} from "./ColorGenerator.js"; +import {randomScheme, contrastChecker} from "./ColorGenerator.js"; import {sumArr} from "../utils.js"; import {alignment, semanticsEmphasis, whiteSpaceFraction} from "../../@evoposter/evaluator/src/index.mjs"; +import * as config from './../../../evo-poster.config.js'; +const MAX_COLOR_SCHEME_ATTEMPT = config["default"]["COLOR"] !== undefined ? config["default"]["COLOR"]["MAX_COLOR_SCHEME_ATTEMPT"] : 200; + class Poster { #showGrid = false; @@ -79,8 +82,16 @@ class Poster { return new Poster(this.n, this.generation, this.params, genotypeCopy); } - #generateGenotype = (params) => { - const colorScheme = randomScheme(); + #generateGenotype = (params, ) => { + // generate scheme + let colorContrast = false; + let colorScheme; + let colorAttempt = 0; + while (!colorContrast || colorAttempt > MAX_COLOR_SCHEME_ATTEMPT) { + colorScheme = randomScheme(); + colorContrast = contrastChecker(colorScheme["baseColour"], colorScheme["colorA"], colorScheme["colorB"]); + colorAttempt++; + } // define grid const grid = new Grid( diff --git a/src/public/app.js b/src/public/app.js index 70f794e..7e3f3dd 100644 --- a/src/public/app.js +++ b/src/public/app.js @@ -5899,7 +5899,7 @@ const arrMean = (arr) => { }; -const hexToRGB = (hex) => { +const hexToRGB$1 = (hex) => { if (hex["levels"]) { return { r: parseInt(hex["levels"][0]), @@ -6081,18 +6081,13 @@ const OPTIMAL = .5; const MIN_DISTANCE = 10; const compute = (img, color, amount = null, optimal = OPTIMAL) => { + if (amount === null) { + color = hexToRGB$1(color); + amount = percentTypographyColor(img, color, img.pixelDensity()); + } - console.group(); - console.log ("COLOR", color); - - color = hexToRGB(color); - amount = amount === null ? percentTypographyColor(img, color, img.pixelDensity()) : amount; const res = 1-4*Math.pow((amount - optimal), 2); - console.log ("AMOUNT", amount); - console.log ("RES", res); - console.groupEnd(); - return res; }; @@ -6595,13 +6590,87 @@ const complement = (baseColor, key) => { var complement$1 = complement; +const TYPEFACES = { + Amstelvar: { + leading: 1.05, + tags: [`serif`], + axes: [`wght`, `wdth`], + url: `https://github.com/googlefonts/amstelvar` + }, + Anybody: { + leading: 1.05, + tags: [`sans-serif`, `90s`, `europe`], + axes: [`wght`, `wdth`], + url: `https://github.com/Etcetera-Type-Co/Anybody`, + }, + Barlow: { + leading: 1.05, + tags: [`sans-serif`, `gothic`, `monoline`, `neo-grotesque`], + axes: [`wght`, `wdth`], + url: `https://tribby.com/fonts/barlow/`, + }, + Cabin: { + leading: 1.05, + tags: [`sans-serif`, `gothic`, `soft-corners`], + axes: [`wght`], + url: `https://fonts.google.com/specimen/Cabin`, + }, + Emberly: { + leading: 1.05, + tags: [`serif`, `didone`], + axes: [`wght`, `wdth`], + url: `https://www.behance.net/gallery/87667103/Emberly-Free-Typeface-54-Styles`, + }, + Epilogue: { + leading: 1.05, + tags: [`sans-serif`], + axes: [`wght`, `wdth`], + url: `https://etceteratype.co/epilogue`, + }, + IBMPlexSans: { + leading: 1.05, + tags: [`sans-serif`], + axes: [`wght`, `wdth`], + url: `https://fonts.google.com/specimen/IBM+Plex+Sans`, + }, + Inconsolata: { + leading: 1.05, + tags: [`sans-serif`, `mono`], + axes: [`wght`, `wdth`], + url: `https://fonts.google.com/specimen/Inconsolata`, + + } +}; + +const COLOR = { + MIN_CONTRAST: 2.5, + MAX_COLOR_SCHEME_ATTEMPT: 200, +}; + + +var evoPoster_config = { + typography: TYPEFACES !== undefined ? TYPEFACES : {}, + color: COLOR !== undefined ? COLOR : {} +}; + +const MIN_CONTRAST = evoPoster_config.color !== null ? evoPoster_config.color.MIN_CONTRAST : 10; + const randomScheme = () => { const baseColour = randomColour(); return complementAndAnalogueScheme(baseColour); }; +const contrastChecker = (baseColor, colorA, colorB) => { + const baseColorLuminance = luminance(baseColor); + const colorALuminance = luminance(colorA); + const colorBLuminance = luminance(colorB); + const min = Math.min(contrastRatio(baseColorLuminance, colorALuminance), contrastRatio(baseColorLuminance, colorBLuminance)); + return min > MIN_CONTRAST; +}; + const complementAndAnalogueScheme = (baseColour) => { const colorA = complement$1(baseColour, 1); + const colorB = analogue$1(colorA, Math.round(-2+(Math.random()*4))); return { baseColour: baseColour, @@ -6616,6 +6685,38 @@ const randomColour = () => { return `#${hex.join("")}`; }; + + +const hexToRGB = (hex) => { + let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +}; + +const luminance = (c) => { + c = hexToRGB(c); + let [lumR, lumG, lumB] = Object.keys(c).map(key => { + let proportion = c[key] / 255; + return proportion <= 0.03928 + ? proportion / 12.92 + : Math.pow((proportion + 0.055) / 1.055, 2.4); + }); + return 0.2126 * lumR + 0.7152 * lumG + 0.0722 * lumB; +}; + +const contrastRatio = (luminance1, luminance2) => { + let lighterLum = Math.max(luminance1, luminance2); + let darkerLum = Math.min(luminance1, luminance2); + + return (lighterLum + 0.05) / (darkerLum + 0.05); +}; + +const MAX_COLOR_SCHEME_ATTEMPT$1 = evoPoster_config["COLOR"] !== undefined ? evoPoster_config["COLOR"]["MAX_COLOR_SCHEME_ATTEMPT"] : 200; + + class Poster { #showGrid = false; #debug = false; @@ -6688,8 +6789,16 @@ class Poster { return new Poster(this.n, this.generation, this.params, genotypeCopy); } - #generateGenotype = (params) => { - const colorScheme = randomScheme(); + #generateGenotype = (params, ) => { + // generate scheme + let colorContrast = false; + let colorScheme; + let colorAttempt = 0; + while (!colorContrast || colorAttempt > MAX_COLOR_SCHEME_ATTEMPT$1) { + colorScheme = randomScheme(); + colorContrast = contrastChecker(colorScheme["baseColour"], colorScheme["colorA"], colorScheme["colorB"]); + colorAttempt++; + } // define grid const grid = new Grid( @@ -7338,6 +7447,7 @@ class Grid { const SIZE_MUTATION_ADJUST = 5; const TOURNAMENT_SIZE = 10; +const MAX_COLOR_SCHEME_ATTEMPT = evoPoster_config["COLOR"] !== undefined ? evoPoster_config["COLOR"]["MAX_COLOR_SCHEME_ATTEMPT"] : 200; class Population { #typefaces; @@ -7529,7 +7639,15 @@ class Population { // colours scheme if (Math.random() < prob) { - const colorScheme = randomScheme(); + let colorContrast = false; + let colorScheme; + let colorAttempt = 0; + while (!colorContrast || colorAttempt > MAX_COLOR_SCHEME_ATTEMPT) { + colorScheme = randomScheme(); + colorContrast = contrastChecker(colorScheme["baseColour"], colorScheme["colorA"], colorScheme["colorB"]); + colorAttempt++; + } + // mutate background colours if (!this.params["background"]["lock"][1]) { ind.genotype["background"]["colors"][0] = colorScheme.colorA; @@ -7824,62 +7942,6 @@ class Population { } } -const TYPEFACES = { - Amstelvar: { - leading: 1.05, - tags: [`serif`], - axes: [`wght`, `wdth`], - url: `https://github.com/googlefonts/amstelvar` - }, - Anybody: { - leading: 1.05, - tags: [`sans-serif`, `90s`, `europe`], - axes: [`wght`, `wdth`], - url: `https://github.com/Etcetera-Type-Co/Anybody`, - }, - Barlow: { - leading: 1.05, - tags: [`sans-serif`, `gothic`, `monoline`, `neo-grotesque`], - axes: [`wght`, `wdth`], - url: `https://tribby.com/fonts/barlow/`, - }, - Cabin: { - leading: 1.05, - tags: [`sans-serif`, `gothic`, `soft-corners`], - axes: [`wght`], - url: `https://fonts.google.com/specimen/Cabin`, - }, - Emberly: { - leading: 1.05, - tags: [`serif`, `didone`], - axes: [`wght`, `wdth`], - url: `https://www.behance.net/gallery/87667103/Emberly-Free-Typeface-54-Styles`, - }, - Epilogue: { - leading: 1.05, - tags: [`sans-serif`], - axes: [`wght`, `wdth`], - url: `https://etceteratype.co/epilogue`, - }, - IBMPlexSans: { - leading: 1.05, - tags: [`sans-serif`], - axes: [`wght`, `wdth`], - url: `https://fonts.google.com/specimen/IBM+Plex+Sans`, - }, - Inconsolata: { - leading: 1.05, - tags: [`sans-serif`, `mono`], - axes: [`wght`, `wdth`], - url: `https://fonts.google.com/specimen/Inconsolata`, - - } -}; - -var evoPoster_config = { - typography: TYPEFACES !== undefined ? TYPEFACES : {} -}; - window.preload = () => {}; window.setup = () => {