Catégories
Astuces et Design

Clouer le contraste parfait entre un texte clair et une image d'arrière-plan

Avez-vous déjà rencontré un site où du texte clair se trouve sur une image de fond clair? Si c'est le cas, vous saurez à quel point c'est difficile à lire. Un moyen populaire d'éviter cela consiste à utiliser une superposition transparente. Mais cela conduit à une question importante: à quel point la transparence devrait cette superposition être? Ce n’est pas comme si nous avions toujours affaire aux mêmes tailles de police, poids et couleurs, et, bien sûr, des images différentes entraîneront des contrastes différents.

Essayer de supprimer le mauvais contraste du texte sur les images d'arrière-plan est un peu comme jouer à Whac-a-Mole. Au lieu de deviner, nous pouvons résoudre ce problème avec HTML et un peu de maths.

Comme ça:

On pourrait dire "Problème résolu!" et terminez simplement cet article ici. Mais où est le plaisir? Ce que je veux te montrer c'est Comment cet outil fonctionne et vous avez donc une nouvelle façon de gérer ce problème trop courant.

Voici le plan

Tout d’abord, précisons nos objectifs. Nous avons dit que nous voulions du texte lisible au-dessus d'une image d'arrière-plan, mais que signifie même «lisible»? Pour nos besoins, nous utiliserons la définition WCAG de la lisibilité de niveau AA, qui indique que les couleurs du texte et de l'arrière-plan ont besoin d'un contraste suffisant entre elles pour qu'une couleur soit 4,5 fois plus claire que l'autre.

Choisissons une couleur de texte, une image d'arrière-plan et une couleur de superposition comme point de départ. Compte tenu de ces entrées, nous voulons trouver le niveau d'opacité de la superposition qui rend le texte lisible sans cacher tellement l'image qu'elle est également difficile à voir. Pour compliquer un peu les choses, nous utiliserons une image avec un espace à la fois sombre et clair et nous nous assurerons que la superposition en tient compte.

Notre résultat final sera une valeur que nous pouvons appliquer au CSS opacity propriété de la superposition qui nous donne la bonne quantité de transparence qui rend le texte 4,5 fois plus clair que l'arrière-plan.

Opacité optimale de la superposition: 0.521

Pour trouver l'opacité optimale de la superposition, nous allons suivre quatre étapes:

  1. Nous mettrons l'image dans un HTML , qui nous permettra de lire les couleurs de chaque pixel de l'image.
  2. Nous trouverons le pixel de l’image qui contraste le moins avec le texte.
  3. Ensuite, nous allons préparer une formule de mélange de couleurs que nous pouvons utiliser pour tester différents niveaux d'opacité en plus de la couleur de ce pixel.
  4. Enfin, nous ajusterons l'opacité de notre superposition jusqu'à ce que le contraste du texte atteigne l'objectif de lisibilité. Et ce ne sera pas seulement des suppositions aléatoires – nous utiliserons des techniques de recherche binaire pour accélérer ce processus.

Commençons!

Étape 1: Lire les couleurs de l'image sur la toile

Canvas permet de «lire» les couleurs contenues dans une image. Pour ce faire, nous devons «dessiner» l'image sur un élément, puis utilisez le contexte de canevas (ctx) getImageData() méthode pour produire une liste des couleurs de l’image.

function getImagePixelColorsUsingCanvas(image, canvas) {
  // The canvas's context (often abbreviated as ctx) is an object
  // that contains a bunch of functions to control your canvas
  const ctx = canvas.getContext('2d');


  // The width can be anything, so I picked 500 because it's large
  // enough to catch details but small enough to keep the
  // calculations quick.
  canvas.width = 500;


  // Make sure the canvas matches proportions of our image
  canvas.height = (image.height / image.width) * canvas.width;


  // Grab the image and canvas measurements so we can use them in the next step
  const sourceImageCoordinates = (0, 0, image.width, image.height);
  const destinationCanvasCoordinates = (0, 0, canvas.width, canvas.height);


  // Canvas's drawImage() works by mapping our image's measurements onto
  // the canvas where we want to draw it
  ctx.drawImage(
    image,
    ...sourceImageCoordinates,
    ...destinationCanvasCoordinates
  );


  // Remember that getImageData only works for same-origin or 
  // cross-origin-enabled images.
  // https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
  const imagePixelColors = ctx.getImageData(...destinationCanvasCoordinates);
  return imagePixelColors;
}

le getImageData() La méthode nous donne une liste de nombres représentant les couleurs de chaque pixel. Chaque pixel est représenté par quatre nombres: rouge, vert, bleu et opacité (également appelé «alpha»). Sachant cela, nous pouvons parcourir la liste des pixels et trouver toutes les informations dont nous avons besoin. Cela sera utile à l'étape suivante.

Image d'une rose bleue et violette sur fond rose clair. Une section de la rose est agrandie pour révéler les valeurs RGBA d'un pixel spécifique.

Étape 2: Trouvez le pixel avec le moins de contraste

Avant de faire cela, nous devons savoir comment calculer le contraste. Nous allons écrire une fonction appelée getContrast() qui prend deux couleurs et crache un nombre représentant le niveau de contraste entre les deux. Plus le nombre est élevé, meilleur est le contraste pour la lisibilité.

Quand j'ai commencé à rechercher des couleurs pour ce projet, je m'attendais à trouver une formule simple. Il s'est avéré qu'il y avait plusieurs étapes.

Pour calculer le contraste entre deux couleurs, nous devons connaître leurs niveaux de luminance, qui est essentiellement la luminosité (Stacie Arellano fait une analyse approfondie de la luminance qui vaut la peine d'être vérifiée.)

Grâce au W3C, nous connaissons la formule de calcul du contraste en utilisant la luminance:

const contrast = (lighterColorLuminance + 0.05) / (darkerColorLuminance + 0.05);

Obtenir la luminance d’une couleur signifie que nous devons convertir la couleur de la valeur RVB 8 bits standard utilisée sur le Web (où chaque couleur est comprise entre 0 et 255) en ce que l’on appelle linéaire RVB. La raison pour laquelle nous devons faire cela est que la luminosité n’augmente pas uniformément lorsque les couleurs changent. Nous devons convertir nos couleurs dans un format où la luminosité varie uniformément avec les changements de couleur. Cela nous permet de calculer correctement la luminance. Encore une fois, le W3C est une aide ici:

const luminance = (0.2126 * getLinearRGB(r) + 0.7152 * getLinearRGB(g) + 0.0722 * getLinearRGB(b));

Mais attendez, il y en a plus! Pour convertir un RVB 8 bits (0 à 255) en RVB linéaire, nous devons passer par ce qu'on appelle le RVB standard (également appelé sRVB), qui est sur une échelle de 0 à 1.

Le processus se déroule donc:

8-bit RGB → standard RGB  → linear RGB → luminance

Et une fois que nous avons la luminance des deux couleurs que nous voulons comparer, nous pouvons brancher les valeurs de luminance pour obtenir le contraste entre leurs couleurs respectives.

// getContrast is the only function we need to interact with directly.
// The rest of the functions are intermediate helper steps.
function getContrast(color1, color2) {
  const color1_luminance = getLuminance(color1);
  const color2_luminance = getLuminance(color2);
  const lighterColorLuminance = Math.max(color1_luminance, color2_luminance);
  const darkerColorLuminance = Math.min(color1_luminance, color2_luminance);
  const contrast = (lighterColorLuminance + 0.05) / (darkerColorLuminance + 0.05);
  return contrast;
}


function getLuminance({r,g,b}) {
  return (0.2126 * getLinearRGB(r) + 0.7152 * getLinearRGB(g) + 0.0722 * getLinearRGB(b));
}
function getLinearRGB(primaryColor_8bit) {
  // First convert from 8-bit rbg (0-255) to standard RGB (0-1)
  const primaryColor_sRGB = convert_8bit_RGB_to_standard_RGB(primaryColor_8bit);


  // Then convert from sRGB to linear RGB so we can use it to calculate luminance
  const primaryColor_RGB_linear = convert_standard_RGB_to_linear_RGB(primaryColor_sRGB);
  return primaryColor_RGB_linear;
}
function convert_8bit_RGB_to_standard_RGB(primaryColor_8bit) {
  return primaryColor_8bit / 255;
}
function convert_standard_RGB_to_linear_RGB(primaryColor_sRGB) {
  const primaryColor_linear = primaryColor_sRGB < 0.03928 ?
    primaryColor_sRGB/12.92 :
    Math.pow((primaryColor_sRGB + 0.055) / 1.055, 2.4);
  return primaryColor_linear;
}

Maintenant que nous pouvons calculer le contraste, nous devrons examiner notre image de l'étape précédente et parcourir chaque pixel en boucle, en comparant le contraste entre la couleur de ce pixel et la couleur du texte de premier plan. Au fur et à mesure que nous parcourons les pixels de l'image, nous suivrons le pire contraste (le plus bas) à ce jour, et lorsque nous atteindrons la fin de la boucle, nous connaîtrons la couleur de contraste le plus défavorable de l'image.

function getWorstContrastColorInImage(textColor, imagePixelColors) {
  let worstContrastColorInImage;
  let worstContrast = Infinity; // This guarantees we won't start too low
  for (let i = 0; i < imagePixelColors.data.length; i += 4) {
    let pixelColor = {
      r: imagePixelColors.data(i),
      g: imagePixelColors.data(i + 1),
      b: imagePixelColors.data(i + 2),
    };
    let contrast = getContrast(textColor, pixelColor);
    if(contrast < worstContrast) {
      worstContrast = contrast;
      worstContrastColorInImage = pixelColor;
    }
  }
  return worstContrastColorInImage;
}

Étape 3: Préparez une formule de mélange de couleurs pour tester les niveaux d'opacité de la superposition

Maintenant que nous connaissons la couleur de contraste le plus défavorable de notre image, l'étape suivante consiste à déterminer le degré de transparence de la superposition et à voir comment cela change le contraste avec le texte.

Lorsque j'ai implémenté cela pour la première fois, j'ai utilisé une toile séparée pour mélanger les couleurs et lire les résultats. Cependant, grâce à l'article d'Ana Tudor sur la transparence, je sais maintenant qu'il existe une formule pratique pour calculer la couleur résultante du mélange d'une couleur de base avec une superposition transparente.

Pour chaque canal de couleur (rouge, vert et bleu), nous appliquerons cette formule pour obtenir la couleur mélangée:

mixedColor = baseColor + (overlayColor - baseColor) * overlayOpacity

Donc, dans le code, cela ressemblerait à ceci:

function mixColors(baseColor, overlayColor, overlayOpacity) {
  const mixedColor = {
    r: baseColor.r + (overlayColor.r - baseColor.r) * overlayOpacity,
    g: baseColor.g + (overlayColor.g - baseColor.g) * overlayOpacity,
    b: baseColor.b + (overlayColor.b - baseColor.b) * overlayOpacity,
  }
  return mixedColor;
}

Maintenant que nous sommes en mesure de mélanger les couleurs, nous pouvons tester le contraste lorsque la valeur d'opacité de la superposition est appliquée.

function getTextContrastWithImagePlusOverlay({textColor, overlayColor, imagePixelColor, overlayOpacity}) {
  const colorOfImagePixelPlusOverlay = mixColors(imagePixelColor, overlayColor, overlayOpacity);
  const contrast = getContrast(this.state.textColor, colorOfImagePixelPlusOverlay);
  return contrast;
}

Avec cela, nous avons tous les outils dont nous avons besoin pour trouver l'opacité de superposition optimale!

Étape 4: Trouvez l'opacité de la superposition qui atteint notre objectif de contraste

Nous pouvons tester l'opacité d'une superposition et voir comment cela affecte le contraste entre le texte et l'image. Nous allons essayer un tas de niveaux d'opacité différents jusqu'à ce que nous trouvions le contraste qui frappe notre marque là où le texte est 4,5 fois plus clair que l'arrière-plan. Cela peut sembler fou, mais ne vous inquiétez pas; nous n’allons pas deviner au hasard. Nous utiliserons une recherche binaire, qui est un processus qui nous permet d'affiner rapidement l'ensemble des réponses possibles jusqu'à ce que nous obtenions un résultat précis.

Voici comment fonctionne une recherche binaire:

  • Devinez au milieu.
  • Si la supposition est trop élevée, nous éliminons la moitié supérieure des réponses. Trop bas? Nous éliminons la moitié inférieure à la place.
  • Devinez au milieu de cette nouvelle gamme.
  • Répétez ce processus jusqu'à ce que nous obtenions une valeur.

J'ai juste un outil pour montrer comment cela fonctionne:

Dans ce cas, nous essayons de deviner une valeur d'opacité comprise entre 0 et 1. Nous devinerons donc au milieu, testerons si le contraste résultant est trop élevé ou trop faible, éliminons la moitié des options et devinerons à nouveau. Si nous limitons la recherche binaire à huit estimations, nous obtiendrons une réponse précise en un clin d'œil.

Avant de commencer la recherche, nous aurons besoin d'un moyen de vérifier si une superposition est même nécessaire en premier lieu. Il ne sert à rien d’optimiser une superposition dont nous n’avons même pas besoin!

function isOverlayNecessary(textColor, worstContrastColorInImage, desiredContrast) {
  const contrastWithoutOverlay = getContrast(textColor, worstContrastColorInImage);
  return contrastWithoutOverlay < desiredContrast;
}

Nous pouvons maintenant utiliser notre recherche binaire pour rechercher l'opacité optimale de la superposition:

function findOptimalOverlayOpacity(textColor, overlayColor, worstContrastColorInImage, desiredContrast) {
  // If the contrast is already fine, we don't need the overlay,
  // so we can skip the rest.
  const isOverlayNecessary = isOverlayNecessary(textColor, worstContrastColorInImage, desiredContrast);
  if (!isOverlayNecessary) {
    return 0;
  }


  const opacityGuessRange = {
    lowerBound: 0,
    midpoint: 0.5,
    upperBound: 1,
  };
  let numberOfGuesses = 0;
  const maxGuesses = 8;


  // If there's no solution, the opacity guesses will approach 1,
  // so we can hold onto this as an upper limit to check for the no-solution case.
  const opacityLimit = 0.99;


  // This loop repeatedly narrows down our guesses until we get a result
  while (numberOfGuesses < maxGuesses) {
    numberOfGuesses++;


    const currentGuess = opacityGuessRange.midpoint;
    const contrastOfGuess = getTextContrastWithImagePlusOverlay({
      textColor,
      overlayColor,
      imagePixelColor: worstContrastColorInImage,
      overlayOpacity: currentGuess,
    });


    const isGuessTooLow = contrastOfGuess < desiredContrast;
    const isGuessTooHigh = contrastOfGuess > desiredContrast;
    if (isGuessTooLow) {
      opacityGuessRange.lowerBound = currentGuess;
    }
    else if (isGuessTooHigh) {
      opacityGuessRange.upperBound = currentGuess;
    }


    const newMidpoint = ((opacityGuessRange.upperBound - opacityGuessRange.lowerBound) / 2) + opacityGuessRange.lowerBound;
    opacityGuessRange.midpoint = newMidpoint;
  }


  const optimalOpacity = opacityGuessRange.midpoint;
  const hasNoSolution = optimalOpacity > opacityLimit;


  if (hasNoSolution) {
    console.log('No solution'); // Handle the no-solution case however you'd like
    return opacityLimit;
  }
  return optimalOpacity;
}

Une fois notre expérience terminée, nous savons maintenant exactement à quel point notre superposition doit être transparente pour que notre texte reste lisible sans trop masquer l'image d'arrière-plan.

Nous l'avons fait!

Améliorations et limites

Les méthodes que nous avons décrites ne fonctionnent que si la couleur du texte et la couleur de la superposition présentent un contraste suffisant pour commencer. Par exemple, si vous choisissez une couleur de texte identique à celle de votre superposition, il n’y aura pas de solution optimale à moins que l’image ne nécessite pas du tout de superposition.

De plus, même si le contraste est mathématiquement acceptable, cela ne garantit pas toujours qu'il aura fière allure. Cela est particulièrement vrai pour le texte sombre avec une superposition claire et une image d'arrière-plan chargée. Diverses parties de l'image peuvent détourner l'attention du texte, rendant la lecture difficile même lorsque le contraste est numériquement correct. C’est pourquoi la recommandation la plus courante consiste à utiliser du texte clair sur un fond sombre.

Nous n'avons pas non plus pris en compte l'emplacement des pixels ni le nombre de pixels de chaque couleur. Un inconvénient est qu'un pixel dans le coin peut éventuellement exercer une trop grande influence sur le résultat. L’avantage, cependant, est que nous n’avons pas à nous soucier de la façon dont les couleurs de l’image sont distribuées ou de l’emplacement du texte, car tant que nous avons géré le moins de contraste, nous sommes en sécurité partout ailleurs.

J'ai appris quelques choses en cours de route

Il y a des choses avec lesquelles je suis parti après cette expérience, et j'aimerais les partager avec vous:

  • Être précis sur un objectif aide vraiment! Nous avons commencé avec un vague objectif de vouloir du texte lisible sur une image, et nous nous sommes retrouvés avec un niveau de contraste spécifique auquel nous pouvions aspirer.
  • Il est si important d’être clair sur les termes. Par exemple, le RVB standard n'était pas ce à quoi je m'attendais. J'ai appris que ce que je considérais comme RVB «régulier» (0 à 255) est officiellement appelé RVB 8 bits. De plus, je pensais que le «L» dans les équations que j'avais recherchées signifiait «légèreté», mais il signifie en fait «luminance», ce qui ne doit pas être confondu avec «luminosité». La clarification des termes nous aide à coder et à discuter du résultat final.
  • Complexe ne veut pas dire insoluble. Les problèmes qui semblent durs peuvent être divisés en éléments plus petits et plus faciles à gérer.
  • Lorsque vous marchez sur le chemin, vous repérez les raccourcis. Dans le cas courant du texte blanc sur une superposition transparente noire, vous n'aurez jamais besoin d'une opacité supérieure à 0,54 pour obtenir une lisibilité de niveau AA WCAG.

En résumé…

Vous avez maintenant un moyen de rendre votre texte lisible sur une image d'arrière-plan sans sacrifier trop d'image. Si vous êtes arrivé jusqu'ici, j'espère avoir été en mesure de vous donner une idée générale de la façon dont tout cela fonctionne.

J'ai commencé ce projet à l'origine parce que j'ai vu (et créé) trop de bannières de sites Web où le texte était difficile à lire sur une image d'arrière-plan ou l'image d'arrière-plan était trop obscurcie par la superposition. Je voulais faire quelque chose à ce sujet, et je voulais donner aux autres un moyen de faire de même. J'ai écrit cet article dans l'espoir que vous repartirez avec une meilleure compréhension de la lisibilité sur le Web. J'espère que vous avez également appris quelques astuces de canevas.

Si vous avez fait quelque chose d'intéressant avec la lisibilité ou le canevas, j'aimerais beaucoup en entendre parler dans les commentaires!

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *