Table of Contents
plan
Step 1: Read the image color from the canvas
Step 2: Find the pixel with the least contrast
Step 3: Prepare the color mixing formula to test the overlay opacity level
Step 4: Find the overlay opacity that reaches the contrast target
Improvements and limitations
I learned something along the way
Home Web Front-end CSS Tutorial Nailing the Perfect Contrast Between Light Text and a Background Image

Nailing the Perfect Contrast Between Light Text and a Background Image

Apr 03, 2025 am 09:44 AM

Nailing the Perfect Contrast Between Light Text and a Background Image

Have you ever encountered light text on a website superimposed on a light background image? If you have encountered it, you will know how difficult it is to read. One common way to avoid this is to use a transparent overlay. But this brings up an important question: How high should the transparency of the overlay be? We don't always deal with the same font size, thickness, and color, and of course, different pictures will also produce different contrast.

Try to eliminate the problem of poor text contrast on the background image, just like playing the goblin game. Rather than guessing, use HTML<canvas></canvas> And some mathematical methods to solve this problem.

Like this:

We can say "the problem is solved!" and then end this article. But what's the fun of this? What I want to show you is how this tool works so that you can master a new way to deal with this common problem.

plan

First, let’s clarify our goals. We said that we want to display readable text on the background image, but what exactly does "readable" mean? For our purposes, we will use WCAG's definition of AA-level readability, which states that there is a need for sufficient contrast between text and background colors so that one color is 4.5 times brighter than the other.

Let's choose a text color, a background image, and an overlay color as the starting point. Given these inputs, we want to find an overlay opacity level that makes the text readable without hiding the image so much that the image is difficult to see. To complicate things a little bit, we'll use a picture that has both dark and light colors, and make sure the overlay takes that into account.

Our end result will be a value that we can apply to the overlay's CSS opacity property, which makes the text 4.5 times brighter than the background.

To find the best overlay opacity, we will perform four steps:

  1. We put the image into HTML<canvas></canvas> This will allow us to read the color of each pixel in the picture.
  2. We will find the pixels in the picture that have the least contrast to the text.
  3. Next, we will prepare a color mixing formula that we can use to test the effect of different opacity levels on the color of that pixel.
  4. Finally, we will adjust the opacity of the overlay until the text contrast reaches the readability goal. This won't be random guessing – we'll use binary search techniques to speed up this process.

Let's get started!

Step 1: Read the image color from the canvas

Canvas allows us to "read" the colors contained in the image. To do this, we need to "draw" the image to<canvas></canvas> On the element, then use getImageData() method of the canvas context (ctx) to generate a list of image colors.

 function getImagePixelColorsUsingCanvas(image, canvas) {
  // The context of canvas (usually abbreviated as ctx) is an object containing many functions to control your canvas
  const ctx = canvas.getContext('2d');

  // The width can be any value, so I chose 500 because it's big enough to capture details, but small enough to make the calculation faster.
  canvas.width = 500;

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

  // Get the measurements of the image and canvas so that 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' drawImage() works by mapping the measurements of our image to the canvas we want to draw it on ctx.drawImage(
    image,
    ...sourceImageCoordinates,
    ...destinationCanvasCoordinates
  );

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

getImageData() method provides us with a list of numbers representing the color of each pixel. Each pixel is represented by four numbers: red, green, blue, and opacity (also known as "alpha". Knowing this, we can iterate through the pixel list and find any information we need. This will be very useful in the next step.

Step 2: Find the pixel with the least contrast

Before that, we need to know how to calculate contrast. We will write a function called getContrast() that takes two colors and outputs a number indicating the level of contrast between the two colors. The higher the numbers, the better the contrast and the better the readability.

When I started researching the colors of this project, I expected to find a simple formula. It turned out that there were multiple steps.

To calculate the contrast between two colors, we need to know their brightness level, which is essentially brightness (Stacie Arellano has an in-depth look at brightness, which is worth a look).

Thanks to W3C, we know the formula for calculating contrast using brightness:

 const contrast = (lighterColorLuminance 0.05) / (darkerColorLuminance 0.05);
Copy after login

Getting the brightness of the color means we have to convert the color from the regular 8-bit RGB value used on the network (where each color is 0-255) to what is called linear RGB. We need to do this because the brightness does not increase evenly with the color change. We need to convert the color to a format where the brightness changes evenly with the color. This allows us to calculate the brightness correctly. Similarly, W3C provides help here:

 const luminance = (0.2126 * getLinearRGB(r) 0.7152 * getLinearRGB(g) 0.0722 * getLinearRGB(b));
Copy after login

But wait, there are more! In order to convert 8-bit RGB (0 to 255) to linear RGB, we need to go through what is called standard RGB (also known as sRGB), which has a ratio of 0 to 1.

Therefore, the process is as follows:

 <code>8位RGB → 标准RGB → 线性RGB → 亮度</code>
Copy after login

Once we have the brightness of the two colors we want to compare, we can substitute the brightness values ​​into the formula to get the contrast between their respective colors.

 // getContrast is the only function we need to interact directly.
// The rest of the functions are intermediate auxiliary 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 rgb (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 that we can use it to calculate the brightness 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 <p> Now that we can calculate the contrast, we need to look at the image in the previous step and iterate through each pixel to compare the contrast between the color of that pixel and the color of the foreground text. When we traverse the pixels of the image, we will track the worst (lowest) contrast so far, and when we reach the end of the loop, we will know the color with the worst contrast in the image.</p><pre class="brush:php;toolbar:false"> function getWorstContrastColorInImage(textColor, imagePixelColors) {
  let worstContrastColorInImage;
  let worstContrast = Infinity; // This ensures that we don't start with a value that is too low for (let i = 0; i <p></p><h3 id="Step-Prepare-the-color-mixing-formula-to-test-the-overlay-opacity-level"> Step 3: Prepare the color mixing formula to test the overlay opacity level</h3><p></p><p> Now that we know the color with the worst contrast in our image, the next step is to determine how high the transparency of the overlay should be and see how this will change the contrast with the text.</p><p></p><p> When I first implemented this I used a separate canvas to mix colors and read the results. However, thanks to Ana Tudor's article on transparency, I now know there is a convenient formula to calculate the resultant color after mixing the base color with the transparent overlay.</p><p></p><p> For each color channel (red, green, and blue), we will apply this formula to get the blended colors:</p><p> Mixed Color = Basic Color (overlapping Color - Basic Color) * Overlapping Opacity</p><p></p><p> So, in the code, this will look like this:</p><pre class="brush:php;toolbar:false"> 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;
}
Copy after login

Now that we can mix colors, we can test the contrast when applying overlay opacity values.

 function getTextContrastWithImagePlusOverlay({textColor, overlayColor, imagePixelColor, overlayOpacity}) {
  const colorOfImagePixelPlusOverlay = mixColors(imagePixelColor, overlayColor, overlayOpacity);
  const contrast = getContrast(textColor, colorOfImagePixelPlusOverlay);
  return contrast;
}
Copy after login

With this we have all the tools we need to find the best overlay opacity!

Step 4: Find the overlay opacity that reaches the contrast target

We can test the opacity of the overlay and see how this will affect the contrast between the text and the image. We will try many different opacity levels until we find a value that reaches the target contrast, where the text is 4.5 times brighter than the background. This may sound crazy, but don't worry; we won't guess randomly. We will use binary search, a process that allows us to quickly narrow down possible sets of answers until we get accurate results.

Here is how binary search works:

 <code>- 在中间猜测。 - 如果猜测过高,我们将消除答案的上半部分。太低了吗?我们将改为消除下半部分。 - 在新的范围中间猜测。 - 重复此过程,直到我们得到一个值。我碰巧有一个工具可以展示它是如何工作的:在这种情况下,我们试图猜测一个介于0和1之间的不透明度值。因此,我们将从中间猜测,测试结果对比度是太高还是太低,消除一半的选项,然后再次猜测。如果我们将二分查找限制为八次猜测,我们将立即得到一个精确的答案。在我们开始搜索之前,我们需要一种方法来检查是否根本需要叠加层。我们根本不需要优化我们不需要的叠加层! ```javascript function isOverlayNecessary(textColor, worstContrastColorInImage, desiredContrast) { const contrastWithoutOverlay = getContrast(textColor, worstContrastColorInImage); return contrastWithoutOverlay </code>
Copy after login

Now we can use binary search to find the best overlay opacity:

 function findOptimalOverlayOpacity(textColor, overlayColor, worstContrastColorInImage, desiredContrast) {
  // If the contrast is good enough, we don't need to 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 is no solution, the opacity guess will be close to 1,
  // So we can use it as the upper limit to check for the situation without solutions.
  const opacityLimit = 0.99;

  // This loop repeatedly narrows down our guess until we get the result while (numberOfGuesses  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 unsolvable situations as needed return opacityLimit;
  }
  return optimalOpacity;
}
Copy after login

Once the experiment is done, we now know exactly how transparent the overlay needs to make the text readable without hiding too many background images.

We did it!

Improvements and limitations

The method we introduce is only effective if the text color and the overlay color themselves have sufficient contrast. For example, if you choose the same text color as the overlay, there will be no optimal solution unless the image does not require an overlay at all.

Also, even though contrast is mathematically acceptable, this doesn't always guarantee it looks great. This is especially true for dark text with light overlays and busy background images. Parts of the image may distract from the text, and may be difficult to read even if the contrast is numerically good. That's why the popular advice is to use light text on dark backgrounds.

We also did not consider the pixel position or the number of pixels per color. One disadvantage of this is that the pixels in the corners can have an excessive impact on the results. But the benefit is that we don't have to worry about how the colors of the image are distributed or where the text is, because as long as we deal with places with the least contrast, we can be safe anywhere else.

I learned something along the way

After this experiment, I have gained something and I want to share it with you:

 <code>- **明确目标非常有帮助!**我们从一个模糊的目标开始,即想要在图像上显示可读的文本,最终得到了一个我们可以努力达到的特定对比度级别。 - **明确术语非常重要。**例如,标准RGB并非我所期望的。我了解到,我认为的“常规”RGB(0到255)正式称为8位RGB。此外,我认为我研究的方程式中的“L”表示“亮度”,但它实际上表示“亮度”,这不能与“光度”混淆。澄清术语有助于我们编写代码以及讨论最终结果。 - **复杂并不意味着无法解决。**听起来很困难的问题可以分解成更小、更容易管理的部分。 - **当你走过这条路时,你会发现捷径。**对于白色文本在黑色透明叠加层上的常见情况,您永远不需要超过0.54的不透明度即可达到WCAG AA级可读性。 ### 总结…您现在有了一种方法可以在背景图像上使文本可读,而不会牺牲过多的图像。如果您已经读到这里,我希望我已经能够让您大致了解其工作原理。我最初开始这个项目是因为我看到(并制作了)太多网站横幅,其中文本在背景图像上难以阅读,或者背景图像被叠加层过度遮挡。我想做些什么,我想给其他人提供一种同样的方法。我写这篇文章是为了希望你们能够更好地理解网络上的可读性。我希望你们也学习了一些很酷的canvas技巧。如果您在可读性或canvas方面做了一些有趣的事情,我很乐意在评论中听到您的想法!</code>
Copy after login

The above is the detailed content of Nailing the Perfect Contrast Between Light Text and a Background Image. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Vue 3 Vue 3 Apr 02, 2025 pm 06:32 PM

It&#039;s out! Congrats to the Vue team for getting it done, I know it was a massive effort and a long time coming. All new docs, as well.

Building an Ethereum app using Redwood.js and Fauna Building an Ethereum app using Redwood.js and Fauna Mar 28, 2025 am 09:18 AM

With the recent climb of Bitcoin’s price over 20k $USD, and to it recently breaking 30k, I thought it’s worth taking a deep dive back into creating Ethereum

Can you get valid CSS property values from the browser? Can you get valid CSS property values from the browser? Apr 02, 2025 pm 06:17 PM

I had someone write in with this very legit question. Lea just blogged about how you can get valid CSS properties themselves from the browser. That&#039;s like this.

Stacked Cards with Sticky Positioning and a Dash of Sass Stacked Cards with Sticky Positioning and a Dash of Sass Apr 03, 2025 am 10:30 AM

The other day, I spotted this particularly lovely bit from Corey Ginnivan’s website where a collection of cards stack on top of one another as you scroll.

A bit on ci/cd A bit on ci/cd Apr 02, 2025 pm 06:21 PM

I&#039;d say "website" fits better than "mobile app" but I like this framing from Max Lynch:

Comparing Browsers for Responsive Design Comparing Browsers for Responsive Design Apr 02, 2025 pm 06:25 PM

There are a number of these desktop apps where the goal is showing your site at different dimensions all at the same time. So you can, for example, be writing

Using Markdown and Localization in the WordPress Block Editor Using Markdown and Localization in the WordPress Block Editor Apr 02, 2025 am 04:27 AM

If we need to show documentation to the user directly in the WordPress editor, what is the best way to do it?

Why are the purple slashed areas in the Flex layout mistakenly considered 'overflow space'? Why are the purple slashed areas in the Flex layout mistakenly considered 'overflow space'? Apr 05, 2025 pm 05:51 PM

Questions about purple slash areas in Flex layouts When using Flex layouts, you may encounter some confusing phenomena, such as in the developer tools (d...

See all articles